froggo-mission-control 1.1.9 → 1.2.2

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.
@@ -1,16 +1,37 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getDb } from '@/lib/database';
3
3
 
4
4
  export async function GET() {
5
5
  try {
6
6
  const db = getDb();
7
- const state = db.prepare("SELECT value FROM module_state WHERE module_id = 'accounts'").get() as { value: string } | undefined;
8
- if (state?.value) return NextResponse.json(JSON.parse(state.value));
9
- } catch { /* table may not have value column or no matching row */ }
10
- return NextResponse.json([]);
7
+ const row = db.prepare("SELECT value FROM settings WHERE key = 'accounts_list'").get() as { value: string } | undefined;
8
+ if (row?.value) {
9
+ const parsed = JSON.parse(row.value);
10
+ return NextResponse.json(Array.isArray(parsed) ? parsed : []);
11
+ }
12
+ return NextResponse.json([]);
13
+ } catch (error) {
14
+ console.error('GET /api/accounts error:', error);
15
+ return NextResponse.json([]);
16
+ }
11
17
  }
12
18
 
13
- export async function POST(request: Request) {
14
- const body = await request.json();
15
- return NextResponse.json({ success: true, id: `acct-${Date.now()}`, ...body });
19
+ export async function POST(request: NextRequest) {
20
+ try {
21
+ const body = await request.json();
22
+ const db = getDb();
23
+
24
+ const row = db.prepare("SELECT value FROM settings WHERE key = 'accounts_list'").get() as { value: string } | undefined;
25
+ let accounts: unknown[] = [];
26
+ try { if (row?.value) accounts = JSON.parse(row.value); } catch { /* */ }
27
+
28
+ const newAccount = { ...body, id: `acct-${Date.now()}`, createdAt: new Date().toISOString() };
29
+ accounts.push(newAccount);
30
+
31
+ db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('accounts_list', ?)").run(JSON.stringify(accounts));
32
+ return NextResponse.json(newAccount, { status: 201 });
33
+ } catch (error) {
34
+ console.error('POST /api/accounts error:', error);
35
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
36
+ }
16
37
  }
@@ -35,6 +35,10 @@ export async function GET(_req: NextRequest, { params }: Params) {
35
35
  let capabilities: string[] = [];
36
36
  try { capabilities = JSON.parse(agent.capabilities || '[]'); } catch { /* */ }
37
37
 
38
+ const mcpRaw = db.prepare('SELECT value FROM settings WHERE key = ?').get(settingKey(id, 'mcpServers')) as { value: string } | undefined;
39
+ let mcpServers: unknown[] = [];
40
+ try { if (mcpRaw?.value) { const p = JSON.parse(mcpRaw.value); if (Array.isArray(p)) mcpServers = p; } } catch { /* */ }
41
+
38
42
  return NextResponse.json({
39
43
  model: agent.model || 'sonnet',
40
44
  trustTier: agent.trust_tier || 'apprentice',
@@ -42,6 +46,7 @@ export async function GET(_req: NextRequest, { params }: Params) {
42
46
  skills: getSettingArray(db, settingKey(id, 'skills')),
43
47
  tools: getSettingArray(db, settingKey(id, 'tools')),
44
48
  apiKeys: getSettingArray(db, settingKey(id, 'apiKeys')),
49
+ mcpServers,
45
50
  });
46
51
  } catch (error) {
47
52
  console.error('GET /api/agents/:id/config error:', error);
@@ -74,8 +79,8 @@ export async function PATCH(req: NextRequest, { params }: Params) {
74
79
  db.prepare('UPDATE agents SET trust_tier = ? WHERE id = ?').run(body.trustTier, id);
75
80
  }
76
81
 
77
- // skills, tools, apiKeys → settings table
78
- for (const field of ['skills', 'tools', 'apiKeys'] as const) {
82
+ // skills, tools, apiKeys, mcpServers → settings table
83
+ for (const field of ['skills', 'tools', 'apiKeys', 'mcpServers'] as const) {
79
84
  if (Array.isArray(body[field])) {
80
85
  const key = settingKey(id, field);
81
86
  const value = JSON.stringify(body[field]);
@@ -1,22 +1,28 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { runReviewCycle } from '@/lib/claraReviewCron';
2
+ import { runReviewCycle, spawnClaraReview } from '@/lib/claraReviewCron';
3
+ import { getDb } from '@/lib/database';
3
4
 
4
5
  export const dynamic = 'force-dynamic';
5
6
  export const runtime = 'nodejs';
6
7
 
7
- // Trigger Clara review for a specific task or sweep all pending
8
8
  export async function POST(request: NextRequest) {
9
- const body = await request.json().catch(() => ({}));
10
- const { taskId } = body;
9
+ try {
10
+ const body = await request.json().catch(() => ({}));
11
+ const { taskId } = body as { taskId?: string };
11
12
 
12
- if (taskId) {
13
- // Single task: import and use the shared spawn logic
14
- const { runReviewCycle: _ } = await import('@/lib/claraReviewCron');
15
- void _;
16
- // For single-task trigger, just run the full sweep it will pick it up
17
- // (The sweep already deduplicates via inReview set)
18
- }
13
+ if (taskId) {
14
+ const task = getDb()
15
+ .prepare(`SELECT id, title, description, assignedTo, progress, lastAgentUpdate FROM tasks WHERE id = ? AND status = 'review'`)
16
+ .get(taskId) as Record<string, unknown> | undefined;
17
+ if (!task) return NextResponse.json({ error: 'Task not found or not in review status' }, { status: 404 });
18
+ spawnClaraReview(task);
19
+ return NextResponse.json({ queued: 1, taskId });
20
+ }
19
21
 
20
- const { queued } = runReviewCycle();
21
- return NextResponse.json({ queued });
22
+ const { queued } = runReviewCycle();
23
+ return NextResponse.json({ queued });
24
+ } catch (err) {
25
+ console.error('[clara-review] Error:', err);
26
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
27
+ }
22
28
  }
@@ -1,5 +1,37 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import Anthropic from '@anthropic-ai/sdk';
2
3
 
3
- export async function POST() {
4
- return NextResponse.json({ success: false, error: 'Generate reply not configured' }, { status: 501 });
4
+ export const runtime = 'nodejs';
5
+
6
+ export async function POST(request: NextRequest) {
7
+ try {
8
+ const { message, context, tone = 'professional' } = await request.json();
9
+ if (!message) {
10
+ return NextResponse.json({ error: 'message is required' }, { status: 400 });
11
+ }
12
+
13
+ const apiKey = process.env.ANTHROPIC_API_KEY;
14
+ if (!apiKey) {
15
+ return NextResponse.json({ success: false, error: 'ANTHROPIC_API_KEY not configured' }, { status: 424 });
16
+ }
17
+
18
+ const client = new Anthropic({ apiKey });
19
+ const systemPrompt = `You are a helpful assistant generating a ${tone} reply to a message. Be concise and direct. Only output the reply text, nothing else.`;
20
+ const userPrompt = context
21
+ ? `Context: ${context}\n\nMessage to reply to: ${message}`
22
+ : `Message to reply to: ${message}`;
23
+
24
+ const response = await client.messages.create({
25
+ model: 'claude-haiku-4-5-20251001',
26
+ max_tokens: 512,
27
+ messages: [{ role: 'user', content: userPrompt }],
28
+ system: systemPrompt,
29
+ });
30
+
31
+ const reply = response.content[0]?.type === 'text' ? response.content[0].text : '';
32
+ return NextResponse.json({ success: true, reply });
33
+ } catch (error) {
34
+ console.error('POST /api/chat/generate-reply error:', error);
35
+ return NextResponse.json({ success: false, error: 'Failed to generate reply' }, { status: 500 });
36
+ }
5
37
  }
@@ -1,5 +1,24 @@
1
1
  import { NextResponse } from 'next/server';
2
+ import { getDb } from '@/lib/database';
2
3
 
3
4
  export async function GET() {
4
- return NextResponse.json([]);
5
+ try {
6
+ const db = getDb();
7
+ const rows = db.prepare(`
8
+ SELECT a.id, a.name, a.role,
9
+ COALESCE(SUM(t.costUsd), 0) AS totalSpend,
10
+ COALESCE(SUM(t.inputTokens), 0) AS inputTokens,
11
+ COALESCE(SUM(t.outputTokens), 0) AS outputTokens,
12
+ COUNT(t.id) AS txCount
13
+ FROM agents a
14
+ LEFT JOIN token_usage t ON t.agentId = a.id
15
+ GROUP BY a.id
16
+ ORDER BY totalSpend DESC
17
+ `).all() as Record<string, unknown>[];
18
+
19
+ return NextResponse.json(rows);
20
+ } catch (error) {
21
+ console.error('GET /api/finance/accounts error:', error);
22
+ return NextResponse.json([]);
23
+ }
5
24
  }
@@ -1,5 +1,43 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDb } from '@/lib/database';
2
3
 
3
4
  export async function GET() {
4
- return NextResponse.json({ total: 0, spent: 0, remaining: 0, categories: [] });
5
+ try {
6
+ const db = getDb();
7
+
8
+ const budgetRow = db.prepare("SELECT value FROM settings WHERE key = 'finance_budget_usd'").get() as { value: string } | undefined;
9
+ const budget = budgetRow ? parseFloat(budgetRow.value) : 500;
10
+
11
+ const spendRow = db.prepare("SELECT COALESCE(SUM(costUsd), 0) AS total FROM token_usage").get() as { total: number };
12
+ const spent = spendRow.total ?? 0;
13
+
14
+ const byAgent = db.prepare(`
15
+ SELECT agentId, COALESCE(SUM(costUsd), 0) AS spend
16
+ FROM token_usage GROUP BY agentId ORDER BY spend DESC
17
+ `).all() as { agentId: string; spend: number }[];
18
+
19
+ return NextResponse.json({
20
+ total: budget,
21
+ spent,
22
+ remaining: Math.max(0, budget - spent),
23
+ categories: byAgent.map(r => ({ name: r.agentId, amount: r.spend })),
24
+ });
25
+ } catch (error) {
26
+ console.error('GET /api/finance/budget error:', error);
27
+ return NextResponse.json({ total: 0, spent: 0, remaining: 0, categories: [] });
28
+ }
29
+ }
30
+
31
+ export async function PATCH(request: NextRequest) {
32
+ try {
33
+ const { limit } = await request.json();
34
+ if (typeof limit !== 'number' || limit < 0) {
35
+ return NextResponse.json({ error: 'limit must be a non-negative number' }, { status: 400 });
36
+ }
37
+ getDb().prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('finance_budget_usd', ?)").run(String(limit));
38
+ return NextResponse.json({ ok: true, limit });
39
+ } catch (error) {
40
+ console.error('PATCH /api/finance/budget error:', error);
41
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
42
+ }
5
43
  }
@@ -1,5 +1,31 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDb } from '@/lib/database';
2
3
 
3
- export async function GET() {
4
- return NextResponse.json([]);
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const agentId = searchParams.get('agentId');
8
+ const taskId = searchParams.get('taskId');
9
+ const limit = Math.min(parseInt(searchParams.get('limit') ?? '50', 10), 200);
10
+ const offset = parseInt(searchParams.get('offset') ?? '0', 10);
11
+
12
+ const db = getDb();
13
+ const conditions: string[] = [];
14
+ const params: unknown[] = [];
15
+
16
+ if (agentId) { conditions.push('agentId = ?'); params.push(agentId); }
17
+ if (taskId) { conditions.push('taskId = ?'); params.push(taskId); }
18
+
19
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
20
+ const rows = db.prepare(
21
+ `SELECT id, agentId, taskId, model, inputTokens, outputTokens, costUsd, createdAt
22
+ FROM token_usage ${where}
23
+ ORDER BY createdAt DESC LIMIT ? OFFSET ?`
24
+ ).all(...params, limit, offset) as Record<string, unknown>[];
25
+
26
+ return NextResponse.json(rows);
27
+ } catch (error) {
28
+ console.error('GET /api/finance/transactions error:', error);
29
+ return NextResponse.json([], { status: 200 });
30
+ }
5
31
  }
@@ -1,5 +1,19 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDb } from '@/lib/database';
2
3
 
3
- export async function GET() {
4
- return NextResponse.json([]);
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const limit = Math.min(parseInt(searchParams.get('limit') ?? '50', 10), 200);
8
+
9
+ const db = getDb();
10
+ const rows = db.prepare(
11
+ `SELECT * FROM inbox WHERE status = 'rejected' ORDER BY updatedAt DESC LIMIT ?`
12
+ ).all(limit) as Record<string, unknown>[];
13
+
14
+ return NextResponse.json(rows);
15
+ } catch (error) {
16
+ console.error('GET /api/inbox/rejections error:', error);
17
+ return NextResponse.json([]);
18
+ }
5
19
  }
@@ -0,0 +1,46 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { existsSync, statSync, openSync, readSync, closeSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ export const dynamic = 'force-dynamic';
7
+ export const runtime = 'nodejs';
8
+
9
+ const LOG_PATH = process.env.LOG_PATH || join(homedir(), 'mission-control', 'logs', 'mission-control.log');
10
+ const MAX_READ = 256 * 1024; // 256 KB max per request
11
+
12
+ export async function GET(request: NextRequest) {
13
+ try {
14
+ const { searchParams } = new URL(request.url);
15
+ const cursor = parseInt(searchParams.get('cursor') ?? '0', 10);
16
+ const limit = Math.min(parseInt(searchParams.get('limit') ?? '200', 10), 1000);
17
+
18
+ if (!existsSync(LOG_PATH)) {
19
+ return NextResponse.json({ file: LOG_PATH, cursor: 0, size: 0, lines: [], truncated: false });
20
+ }
21
+
22
+ const stat = statSync(LOG_PATH);
23
+ const size = stat.size;
24
+ const start = cursor > 0 ? cursor : Math.max(0, size - MAX_READ);
25
+ const readLen = Math.min(size - start, MAX_READ);
26
+
27
+ if (readLen <= 0) {
28
+ return NextResponse.json({ file: LOG_PATH, cursor: size, size, lines: [], truncated: false });
29
+ }
30
+
31
+ const buf = Buffer.alloc(readLen);
32
+ const fd = openSync(LOG_PATH, 'r');
33
+ readSync(fd, buf, 0, readLen, start);
34
+ closeSync(fd);
35
+
36
+ const text = buf.toString('utf-8');
37
+ const rawLines = text.split('\n').filter(Boolean);
38
+ const lines = rawLines.slice(-limit);
39
+ const truncated = rawLines.length > limit;
40
+
41
+ return NextResponse.json({ file: LOG_PATH, cursor: start + readLen, size, lines, truncated });
42
+ } catch (error) {
43
+ console.error('GET /api/logs error:', error);
44
+ return NextResponse.json({ file: LOG_PATH, cursor: 0, size: 0, lines: [], truncated: false });
45
+ }
46
+ }
@@ -1,5 +1,58 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDb } from '@/lib/database';
2
3
 
3
- export async function GET() {
4
- return NextResponse.json([]);
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const status = searchParams.get('status');
8
+ const from = searchParams.get('from');
9
+ const to = searchParams.get('to');
10
+
11
+ const db = getDb();
12
+ const conditions = ["(type LIKE '%meeting%' OR type LIKE '%call%' OR type LIKE '%event%')"];
13
+ const params: unknown[] = [];
14
+
15
+ if (status) { conditions.push('status = ?'); params.push(status); }
16
+ if (from) { conditions.push('scheduledAt >= ?'); params.push(new Date(from).getTime()); }
17
+ if (to) { conditions.push('scheduledAt <= ?'); params.push(new Date(to).getTime()); }
18
+
19
+ const rows = db.prepare(
20
+ `SELECT * FROM scheduled_items WHERE ${conditions.join(' AND ')} ORDER BY scheduledAt ASC`
21
+ ).all(...params) as Record<string, unknown>[];
22
+
23
+ return NextResponse.json(rows);
24
+ } catch (error) {
25
+ console.error('GET /api/meetings error:', error);
26
+ return NextResponse.json([]);
27
+ }
28
+ }
29
+
30
+ export async function POST(request: NextRequest) {
31
+ try {
32
+ const body = await request.json();
33
+ const { title, description, scheduledAt, duration, attendees, type = 'meeting' } = body;
34
+ if (!title || !scheduledAt) {
35
+ return NextResponse.json({ error: 'title and scheduledAt required' }, { status: 400 });
36
+ }
37
+
38
+ const db = getDb();
39
+ const id = `meeting-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
40
+ const now = Date.now();
41
+
42
+ db.prepare(`
43
+ INSERT INTO scheduled_items (id, title, description, type, scheduledAt, duration, attendees, status, createdAt, updatedAt)
44
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'scheduled', ?, ?)
45
+ `).run(
46
+ id, title, description ?? null, type,
47
+ new Date(scheduledAt).getTime(),
48
+ duration ?? 60,
49
+ attendees ? JSON.stringify(attendees) : null,
50
+ now, now
51
+ );
52
+
53
+ return NextResponse.json({ id }, { status: 201 });
54
+ } catch (error) {
55
+ console.error('POST /api/meetings error:', error);
56
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
57
+ }
5
58
  }
@@ -17,3 +17,27 @@ export async function GET(_request: NextRequest) {
17
17
  return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
18
18
  }
19
19
  }
20
+
21
+ export async function PATCH(request: NextRequest) {
22
+ try {
23
+ const db = getDb();
24
+ const body = await request.json();
25
+
26
+ if (typeof body !== 'object' || Array.isArray(body)) {
27
+ return NextResponse.json({ error: 'Body must be a key/value object' }, { status: 400 });
28
+ }
29
+
30
+ const upsert = db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)");
31
+ const upsertAll = db.transaction((entries: [string, string][]) => {
32
+ for (const [key, value] of entries) {
33
+ upsert.run(key, typeof value === 'string' ? value : JSON.stringify(value));
34
+ }
35
+ });
36
+
37
+ upsertAll(Object.entries(body) as [string, string][]);
38
+ return NextResponse.json({ success: true, updated: Object.keys(body).length });
39
+ } catch (error) {
40
+ console.error('PATCH /api/settings error:', error);
41
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
42
+ }
43
+ }
package/bin/cli.js CHANGED
@@ -19,7 +19,7 @@
19
19
  'use strict';
20
20
 
21
21
  const { execSync, spawnSync, spawn } = require('child_process');
22
- const { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } = require('fs');
22
+ const { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync } = require('fs');
23
23
  const path = require('path');
24
24
  const os = require('os');
25
25
  const http = require('http');
@@ -316,6 +316,25 @@ async function cmdSetup(force = false) {
316
316
  }
317
317
  success('Memory vault ready');
318
318
 
319
+ // ── Seed knowledge base ────────────────────────────────────────────────
320
+ const knowledgeSrcDir = path.join(INSTALL_DIR, 'templates', 'knowledge');
321
+ const knowledgeDestDir = path.join(MC_MEMORY, 'knowledge');
322
+ if (existsSync(knowledgeSrcDir)) {
323
+ const knowledgeFiles = readdirSync(knowledgeSrcDir).filter(f => f.endsWith('.md'));
324
+ let seeded = 0;
325
+ for (const file of knowledgeFiles) {
326
+ const dest = path.join(knowledgeDestDir, file);
327
+ if (!existsSync(dest)) {
328
+ try {
329
+ const content = readFileSync(path.join(knowledgeSrcDir, file), 'utf-8');
330
+ writeFileSync(dest, content);
331
+ seeded++;
332
+ } catch { /* skip */ }
333
+ }
334
+ }
335
+ if (seeded > 0) success(`Seeded ${seeded} knowledge base articles`);
336
+ }
337
+
319
338
  // ── Write .env ──────────────────────────────────────────────────────────
320
339
  step('Writing configuration');
321
340
  const resolvedClaude = findClaudeBin();
package/install.sh CHANGED
@@ -771,11 +771,37 @@ StandardError=journal
771
771
  WantedBy=default.target
772
772
  EOF
773
773
 
774
+ # cron daemon systemd service
775
+ cat > "${SYSTEMD_DIR}/mission-control-cron.service" << EOF
776
+ [Unit]
777
+ Description=Mission Control Cron Daemon
778
+ After=network.target
779
+
780
+ [Service]
781
+ Type=simple
782
+ WorkingDirectory=${REPO_DIR}
783
+ ExecStart=${NODE_BIN} ${REPO_DIR}/tools/cron-daemon.js
784
+ Restart=always
785
+ RestartSec=5
786
+ Environment=HOME=${HOME}
787
+ Environment=MC_DB_PATH=${MC_DATA}/mission-control.db
788
+ Environment=SCHEDULE_PATH=${MC_DATA}/schedule.json
789
+ Environment=PROJECT_DIR=${REPO_DIR}
790
+ StandardOutput=journal
791
+ StandardError=journal
792
+
793
+ [Install]
794
+ WantedBy=default.target
795
+ EOF
796
+
774
797
  systemctl --user daemon-reload
775
798
  systemctl --user enable mission-control.service
799
+ systemctl --user enable mission-control-cron.service
776
800
  systemctl --user start mission-control.service
777
- success "systemd service installed and started"
801
+ systemctl --user start mission-control-cron.service
802
+ success "systemd services installed and started (app + cron daemon)"
778
803
  info "Logs: journalctl --user -fu mission-control"
804
+ info "Cron logs: journalctl --user -fu mission-control-cron"
779
805
  else
780
806
  warn "Unknown OS (${OS}) — manual startup only. Run: npm start"
781
807
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "froggo-mission-control",
3
- "version": "1.1.9",
3
+ "version": "1.2.2",
4
4
  "description": "Self-hosted AI agent platform for Claude Code CLI. Multi-agent orchestration, task management, Gmail, Calendar, Kanban and more.",
5
5
  "keywords": [
6
6
  "claude",
@@ -48,7 +48,6 @@
48
48
  "tools/e2e-smoke-test.sh",
49
49
  "tools/db/",
50
50
  "next.config.js",
51
- "next.config.ts",
52
51
  "tsconfig.json",
53
52
  "tailwind.config.js",
54
53
  "tailwind.config.ts",
@@ -59,7 +58,8 @@
59
58
  "APPROVAL_RULES.md",
60
59
  "README.md",
61
60
  "ecosystem.config.js",
62
- "install.sh"
61
+ "install.sh",
62
+ "templates/"
63
63
  ],
64
64
  "scripts": {
65
65
  "postinstall": "node scripts/postinstall.js",
@@ -104,6 +104,9 @@
104
104
  "lucide-react": "^0.303.0",
105
105
  "next": "^16.1.6",
106
106
  "node-gyp": "^12.2.0",
107
+ "postcss": "^8.4.32",
108
+ "autoprefixer": "^10.4.16",
109
+ "tailwindcss": "3.4.17",
107
110
  "react": "^18.2.0",
108
111
  "react-dom": "^18.2.0",
109
112
  "react-draggable": "^4.5.0",
@@ -133,7 +136,6 @@
133
136
  "@typescript-eslint/parser": "^8.0.0",
134
137
  "@vitejs/plugin-react": "^5.1.4",
135
138
  "@vitest/ui": "^4.0.18",
136
- "autoprefixer": "^10.4.16",
137
139
  "eslint": "^8.56.0",
138
140
  "eslint-config-prettier": "^9.1.0",
139
141
  "eslint-plugin-jsx-a11y": "^6.10.2",
@@ -142,9 +144,7 @@
142
144
  "happy-dom": "^20.4.0",
143
145
  "jsdom": "^27.4.0",
144
146
  "playwright": "^1.58.0",
145
- "postcss": "^8.4.32",
146
147
  "prettier": "^3.2.0",
147
- "tailwindcss": "^3.4.0",
148
148
  "typescript": "^5.3.3",
149
149
  "vitest": "^4.0.18",
150
150
  "ws": "^8.19.0"