mop-agent 0.1.14 → 0.1.16
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/README.md +13 -5
- package/apps/web/app/api/apps/route.ts +23 -0
- package/apps/web/app/api/chat/route.ts +26 -4
- package/apps/web/app/api/graph/route.ts +8 -3
- package/apps/web/app/assistant/page.tsx +37 -24
- package/apps/web/app/brain/graph/page.tsx +144 -28
- package/apps/web/app/brain/page.tsx +106 -104
- package/apps/web/app/globals.css +505 -12
- package/apps/web/app/settings/page.tsx +102 -1
- package/apps/web/components/AppShell.tsx +101 -22
- package/apps/web/components/ui/chatgpt-prompt-input.tsx +290 -0
- package/apps/web/components.json +18 -0
- package/apps/web/lib/channels/config.ts +48 -0
- package/apps/web/lib/channels/index.ts +8 -4
- package/apps/web/lib/db/migrate.ts +9 -0
- package/apps/web/lib/providers/anthropic.ts +20 -1
- package/apps/web/lib/providers/openrouter.ts +11 -1
- package/apps/web/lib/providers/types.ts +2 -1
- package/apps/web/package.json +6 -0
- package/apps/web/postcss.config.mjs +5 -0
- package/npm-shrinkwrap.json +1646 -222
- package/package.json +3 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { decryptSecret, encryptSecret, keyHint } from "../crypto";
|
|
2
|
+
import { getSqlite } from "../db/client";
|
|
3
|
+
|
|
4
|
+
export type AppId = "telegram" | "discord" | "whatsapp" | "slack" | "webhook";
|
|
5
|
+
export type AppPayload = { secret: string; fields: Record<string, string> };
|
|
6
|
+
type AppRow = { owner_id: string; app_id: AppId; config_enc: string; enabled: number; updated_at: number };
|
|
7
|
+
|
|
8
|
+
export function listAppConfigs(ownerId: string) {
|
|
9
|
+
const rows = getSqlite().prepare("SELECT * FROM app_config WHERE owner_id = ? ORDER BY app_id").all(ownerId) as AppRow[];
|
|
10
|
+
return rows.map((row) => {
|
|
11
|
+
let hint = "••••";
|
|
12
|
+
try { hint = keyHint((JSON.parse(decryptSecret(row.config_enc)) as AppPayload).secret); } catch { /* masked */ }
|
|
13
|
+
return { appId: row.app_id, configured: true, enabled: !!row.enabled, keyHint: hint, updatedAt: row.updated_at };
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function saveAppConfig(ownerId: string, input: { appId: AppId; secret?: string; fields?: Record<string, string>; enabled: boolean }) {
|
|
18
|
+
const existing = getSqlite().prepare("SELECT * FROM app_config WHERE owner_id = ? AND app_id = ?").get(ownerId, input.appId) as AppRow | undefined;
|
|
19
|
+
let previous: AppPayload | undefined;
|
|
20
|
+
if (existing) {
|
|
21
|
+
try { previous = JSON.parse(decryptSecret(existing.config_enc)) as AppPayload; } catch { /* replace invalid config */ }
|
|
22
|
+
}
|
|
23
|
+
const secret = input.secret?.trim() || previous?.secret;
|
|
24
|
+
if (!secret) throw new Error("missing_secret");
|
|
25
|
+
const payload: AppPayload = { secret, fields: { ...(previous?.fields ?? {}), ...(input.fields ?? {}) } };
|
|
26
|
+
getSqlite().prepare(`
|
|
27
|
+
INSERT INTO app_config(owner_id, app_id, config_enc, enabled, updated_at)
|
|
28
|
+
VALUES (?, ?, ?, ?, ?)
|
|
29
|
+
ON CONFLICT(owner_id, app_id) DO UPDATE SET
|
|
30
|
+
config_enc = excluded.config_enc,
|
|
31
|
+
enabled = excluded.enabled,
|
|
32
|
+
updated_at = excluded.updated_at
|
|
33
|
+
`).run(ownerId, input.appId, encryptSecret(JSON.stringify(payload)), input.enabled ? 1 : 0, Date.now());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function listEnabledAppConfigs(): Array<{ appId: AppId; payload: AppPayload }> {
|
|
37
|
+
const rows = getSqlite().prepare("SELECT * FROM app_config WHERE enabled = 1 ORDER BY updated_at DESC").all() as AppRow[];
|
|
38
|
+
const seen = new Set<AppId>();
|
|
39
|
+
const configs: Array<{ appId: AppId; payload: AppPayload }> = [];
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
if (seen.has(row.app_id)) continue;
|
|
42
|
+
try {
|
|
43
|
+
configs.push({ appId: row.app_id, payload: JSON.parse(decryptSecret(row.config_enc)) as AppPayload });
|
|
44
|
+
seen.add(row.app_id);
|
|
45
|
+
} catch { /* skip unreadable secrets */ }
|
|
46
|
+
}
|
|
47
|
+
return configs;
|
|
48
|
+
}
|
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export async function startChannels(): Promise<string[]> {
|
|
6
6
|
const started: string[] = [];
|
|
7
|
-
|
|
7
|
+
const { listEnabledAppConfigs } = await import("./config");
|
|
8
|
+
const configs = listEnabledAppConfigs();
|
|
9
|
+
const telegramToken = process.env.TELEGRAM_BOT_TOKEN ?? configs.find((config) => config.appId === "telegram")?.payload.secret;
|
|
10
|
+
const discordToken = process.env.DISCORD_BOT_TOKEN ?? configs.find((config) => config.appId === "discord")?.payload.secret;
|
|
11
|
+
if (telegramToken) {
|
|
8
12
|
const { startTelegram } = await import("./telegram");
|
|
9
|
-
startTelegram(
|
|
13
|
+
startTelegram(telegramToken);
|
|
10
14
|
started.push("telegram");
|
|
11
15
|
}
|
|
12
|
-
if (
|
|
16
|
+
if (discordToken) {
|
|
13
17
|
const { startDiscord } = await import("./discord");
|
|
14
|
-
startDiscord(
|
|
18
|
+
startDiscord(discordToken);
|
|
15
19
|
started.push("discord");
|
|
16
20
|
}
|
|
17
21
|
return started;
|
|
@@ -80,6 +80,15 @@ CREATE TABLE IF NOT EXISTS provider_config (
|
|
|
80
80
|
updated_at INTEGER NOT NULL
|
|
81
81
|
);
|
|
82
82
|
|
|
83
|
+
CREATE TABLE IF NOT EXISTS app_config (
|
|
84
|
+
owner_id TEXT NOT NULL,
|
|
85
|
+
app_id TEXT NOT NULL,
|
|
86
|
+
config_enc TEXT NOT NULL,
|
|
87
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
updated_at INTEGER NOT NULL,
|
|
89
|
+
PRIMARY KEY (owner_id, app_id)
|
|
90
|
+
);
|
|
91
|
+
|
|
83
92
|
CREATE TABLE IF NOT EXISTS skill (
|
|
84
93
|
id TEXT PRIMARY KEY,
|
|
85
94
|
name TEXT NOT NULL,
|
|
@@ -7,11 +7,30 @@ export function anthropicProvider(apiKey: string, model = "claude-sonnet-4-6"):
|
|
|
7
7
|
id: "anthropic",
|
|
8
8
|
model,
|
|
9
9
|
async *chat({ system, messages }: ChatOptions) {
|
|
10
|
+
const anthropicMessages: Anthropic.MessageParam[] = messages.map((message) => {
|
|
11
|
+
if (!message.image || message.role !== "user") return { role: message.role, content: message.content };
|
|
12
|
+
const match = message.image.dataUrl.match(/^data:(image\/(?:jpeg|png|gif|webp));base64,(.+)$/s);
|
|
13
|
+
if (!match) return { role: message.role, content: message.content };
|
|
14
|
+
return {
|
|
15
|
+
role: message.role,
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: "image",
|
|
19
|
+
source: {
|
|
20
|
+
type: "base64",
|
|
21
|
+
media_type: match[1] as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
22
|
+
data: match[2]!,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{ type: "text", text: message.content || "Describe and help with this image." },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
});
|
|
10
29
|
const stream = client.messages.stream({
|
|
11
30
|
model,
|
|
12
31
|
max_tokens: 4096,
|
|
13
32
|
system,
|
|
14
|
-
messages:
|
|
33
|
+
messages: anthropicMessages,
|
|
15
34
|
});
|
|
16
35
|
for await (const ev of stream) {
|
|
17
36
|
if (ev.type === "content_block_delta" && ev.delta.type === "text_delta") {
|
|
@@ -8,12 +8,22 @@ export function openRouterProvider(apiKey: string, model = "anthropic/claude-son
|
|
|
8
8
|
id: "openrouter",
|
|
9
9
|
model,
|
|
10
10
|
async *chat({ system, messages }: ChatOptions) {
|
|
11
|
+
const openRouterMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = messages.map((message) => {
|
|
12
|
+
if (!message.image || message.role !== "user") return { role: message.role, content: message.content };
|
|
13
|
+
return {
|
|
14
|
+
role: "user",
|
|
15
|
+
content: [
|
|
16
|
+
{ type: "text", text: message.content || "Describe and help with this image." },
|
|
17
|
+
{ type: "image_url", image_url: { url: message.image.dataUrl } },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
});
|
|
11
21
|
const stream = await client.chat.completions.create({
|
|
12
22
|
model,
|
|
13
23
|
stream: true,
|
|
14
24
|
messages: [
|
|
15
25
|
...(system ? ([{ role: "system" as const, content: system }]) : []),
|
|
16
|
-
...
|
|
26
|
+
...openRouterMessages,
|
|
17
27
|
],
|
|
18
28
|
});
|
|
19
29
|
for await (const chunk of stream) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Provider-neutral chat interface. Adapters stream text deltas. */
|
|
2
|
-
export type
|
|
2
|
+
export type ChatImage = { name: string; mimeType: string; dataUrl: string };
|
|
3
|
+
export type Msg = { role: "user" | "assistant"; content: string; image?: ChatImage };
|
|
3
4
|
|
|
4
5
|
export type ChatOptions = {
|
|
5
6
|
system?: string;
|
package/apps/web/package.json
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@anthropic-ai/sdk": "^0.105.0",
|
|
17
17
|
"@mop/link-protocol": "*",
|
|
18
|
+
"@radix-ui/react-dialog": "^1.1.17",
|
|
19
|
+
"@radix-ui/react-popover": "^1.1.17",
|
|
20
|
+
"@radix-ui/react-tooltip": "^1.2.10",
|
|
18
21
|
"@xenova/transformers": "^2.17.2",
|
|
19
22
|
"better-auth": "^1.6.20",
|
|
20
23
|
"better-sqlite3": "^12.11.1",
|
|
@@ -31,6 +34,7 @@
|
|
|
31
34
|
"ws": "^8.18.0"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
37
|
+
"@tailwindcss/postcss": "^4.3.1",
|
|
34
38
|
"@types/better-sqlite3": "^7.6.13",
|
|
35
39
|
"@types/node": "^22.0.0",
|
|
36
40
|
"@types/react": "^19.0.0",
|
|
@@ -38,7 +42,9 @@
|
|
|
38
42
|
"@types/ws": "^8.5.12",
|
|
39
43
|
"cross-env": "^7.0.3",
|
|
40
44
|
"drizzle-kit": "^0.31.10",
|
|
45
|
+
"tailwindcss": "^4.3.1",
|
|
41
46
|
"tsx": "^4.19.0",
|
|
47
|
+
"tw-animate-css": "^1.4.0",
|
|
42
48
|
"typescript": "^5.5.0"
|
|
43
49
|
}
|
|
44
50
|
}
|