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.
- package/app/api/accounts/route.ts +29 -8
- package/app/api/agents/[id]/config/route.ts +7 -2
- package/app/api/agents/clara/review/route.ts +19 -13
- package/app/api/chat/generate-reply/route.ts +35 -3
- package/app/api/finance/accounts/route.ts +20 -1
- package/app/api/finance/budget/route.ts +40 -2
- package/app/api/finance/transactions/route.ts +29 -3
- package/app/api/inbox/rejections/route.ts +17 -3
- package/app/api/logs/route.ts +46 -0
- package/app/api/meetings/route.ts +56 -3
- package/app/api/settings/route.ts +24 -0
- package/bin/cli.js +20 -1
- package/catalog/agents/mission-control/avatar.webp +0 -0
- package/install.sh +27 -1
- package/package.json +6 -6
- package/src/components/AgentManagementModal.tsx +174 -2
- package/src/lib/claraReviewCron.ts +1 -1
- package/src/lib/gateway.ts +242 -60
- package/src/lib/reviewGate.ts +20 -6
- package/src/store/panelConfig.ts +18 -12
- package/templates/knowledge/agent-dispatch-system.md +62 -0
- package/templates/knowledge/architecture.md +111 -0
- package/templates/knowledge/library-structure.md +82 -0
- package/templates/knowledge/mcp-tools.md +61 -0
- package/templates/knowledge/system-architecture.md +152 -0
- package/templates/knowledge/task-lifecycle.md +61 -0
- package/tools/hooks/approval-hook.js +6 -3
|
@@ -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
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
10
|
-
|
|
9
|
+
try {
|
|
10
|
+
const body = await request.json().catch(() => ({}));
|
|
11
|
+
const { taskId } = body as { taskId?: string };
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
|
Binary file
|
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
|
-
|
|
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.
|
|
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"
|