@cortask/gateway 0.1.0
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/LICENSE +21 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1722 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1722 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { createServer } from "http";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
import cors from "cors";
|
|
6
|
+
import path3 from "path";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import {
|
|
10
|
+
loadConfig,
|
|
11
|
+
getDataDir,
|
|
12
|
+
WorkspaceManager,
|
|
13
|
+
SessionStore,
|
|
14
|
+
migrateAllWorkspaces,
|
|
15
|
+
EncryptedCredentialStore,
|
|
16
|
+
getOrCreateSecret,
|
|
17
|
+
createProvider as createProvider3,
|
|
18
|
+
AgentRunner,
|
|
19
|
+
builtinTools,
|
|
20
|
+
createCronTool,
|
|
21
|
+
createArtifactTool,
|
|
22
|
+
createBrowserTool,
|
|
23
|
+
createSubagentTool,
|
|
24
|
+
createSwitchWorkspaceTool,
|
|
25
|
+
setSubagentRunner,
|
|
26
|
+
cleanupSubagentRecords,
|
|
27
|
+
loadSkills as loadSkills2,
|
|
28
|
+
getEligibleSkills,
|
|
29
|
+
buildSkillTools,
|
|
30
|
+
CronService,
|
|
31
|
+
ArtifactStore,
|
|
32
|
+
UsageStore,
|
|
33
|
+
ModelStore,
|
|
34
|
+
TemplateStore,
|
|
35
|
+
logger as logger2
|
|
36
|
+
} from "@cortask/core";
|
|
37
|
+
import { TelegramAdapter, DiscordAdapter, WhatsAppAdapter as WhatsAppAdapter2 } from "@cortask/channels";
|
|
38
|
+
|
|
39
|
+
// src/routes/workspaces.ts
|
|
40
|
+
import { Router } from "express";
|
|
41
|
+
import path from "path";
|
|
42
|
+
function createWorkspaceRoutes(ctx) {
|
|
43
|
+
const router = Router();
|
|
44
|
+
router.get("/", async (_req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const workspaces = await ctx.workspaceManager.list();
|
|
47
|
+
res.json(workspaces);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
res.status(500).json({ error: String(err) });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
router.get("/:id", async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const workspace = await ctx.workspaceManager.get(req.params.id);
|
|
55
|
+
if (!workspace) {
|
|
56
|
+
res.status(404).json({ error: "Workspace not found" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
res.json(workspace);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
res.status(500).json({ error: String(err) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
router.post("/", async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const { name, rootPath } = req.body;
|
|
67
|
+
if (!name) {
|
|
68
|
+
res.status(400).json({ error: "name is required" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const workspace = await ctx.workspaceManager.create(name, rootPath || void 0);
|
|
72
|
+
res.status(201).json(workspace);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
res.status(500).json({ error: String(err) });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
router.put("/reorder", async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const { ids } = req.body;
|
|
80
|
+
if (!Array.isArray(ids)) {
|
|
81
|
+
res.status(400).json({ error: "ids array required" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await ctx.workspaceManager.reorder(ids);
|
|
85
|
+
res.json({ ok: true });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
res.status(500).json({ error: String(err) });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
router.put("/:id", async (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
await ctx.workspaceManager.update(req.params.id, req.body);
|
|
93
|
+
const updated = await ctx.workspaceManager.get(req.params.id);
|
|
94
|
+
res.json(updated);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
res.status(500).json({ error: String(err) });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
router.delete("/:id", async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
await ctx.workspaceManager.delete(req.params.id);
|
|
102
|
+
res.status(204).send();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
res.status(500).json({ error: String(err) });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
router.post("/:id/open", async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const workspace = await ctx.workspaceManager.open(req.params.id);
|
|
110
|
+
if (!workspace) {
|
|
111
|
+
res.status(404).json({ error: "Workspace not found" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
res.json(workspace);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
res.status(500).json({ error: String(err) });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
router.get("/:id/files/*", async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const workspace = await ctx.workspaceManager.get(req.params.id);
|
|
122
|
+
if (!workspace) {
|
|
123
|
+
res.status(404).json({ error: "Workspace not found" });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const relPath = req.params["0"];
|
|
127
|
+
if (!relPath) {
|
|
128
|
+
res.status(400).json({ error: "File path required" });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const fullPath = path.resolve(workspace.rootPath, relPath);
|
|
132
|
+
if (!fullPath.startsWith(path.resolve(workspace.rootPath))) {
|
|
133
|
+
res.status(403).json({ error: "Path outside workspace" });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
res.sendFile(fullPath, (err) => {
|
|
137
|
+
if (err) {
|
|
138
|
+
res.status(404).json({ error: "File not found" });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
res.status(500).json({ error: String(err) });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return router;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/routes/sessions.ts
|
|
149
|
+
import { Router as Router2 } from "express";
|
|
150
|
+
function createSessionRoutes(ctx) {
|
|
151
|
+
const router = Router2();
|
|
152
|
+
router.get("/", async (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
const workspaceId = req.query.workspaceId;
|
|
155
|
+
if (!workspaceId) {
|
|
156
|
+
res.status(400).json({ error: "workspaceId query param required" });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const workspace = await ctx.workspaceManager.get(workspaceId);
|
|
160
|
+
if (!workspace) {
|
|
161
|
+
res.status(404).json({ error: "Workspace not found" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const store = ctx.getSessionStore(workspace.rootPath);
|
|
165
|
+
const sessions = store.listSessions();
|
|
166
|
+
res.json(sessions);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
res.status(500).json({ error: String(err) });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
router.get("/:id", async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const workspaceId = req.query.workspaceId;
|
|
174
|
+
if (!workspaceId) {
|
|
175
|
+
res.status(400).json({ error: "workspaceId query param required" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const workspace = await ctx.workspaceManager.get(workspaceId);
|
|
179
|
+
if (!workspace) {
|
|
180
|
+
res.status(404).json({ error: "Workspace not found" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const store = ctx.getSessionStore(workspace.rootPath);
|
|
184
|
+
const session = store.getSession(req.params.id);
|
|
185
|
+
if (!session) {
|
|
186
|
+
res.status(404).json({ error: "Session not found" });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
res.json(session);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
res.status(500).json({ error: String(err) });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
router.delete("/:id", async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const workspaceId = req.query.workspaceId;
|
|
197
|
+
if (!workspaceId) {
|
|
198
|
+
res.status(400).json({ error: "workspaceId query param required" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const workspace = await ctx.workspaceManager.get(workspaceId);
|
|
202
|
+
if (!workspace) {
|
|
203
|
+
res.status(404).json({ error: "Workspace not found" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const store = ctx.getSessionStore(workspace.rootPath);
|
|
207
|
+
store.deleteSession(req.params.id);
|
|
208
|
+
res.status(204).send();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
res.status(500).json({ error: String(err) });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return router;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/routes/credentials.ts
|
|
217
|
+
import { Router as Router3 } from "express";
|
|
218
|
+
import { clearSkillCache } from "@cortask/core";
|
|
219
|
+
function createCredentialRoutes(ctx) {
|
|
220
|
+
const router = Router3();
|
|
221
|
+
router.get("/", async (_req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
const keys = await ctx.credentialStore.list();
|
|
224
|
+
res.json(keys);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
res.status(500).json({ error: String(err) });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
router.post("/", async (req, res) => {
|
|
230
|
+
try {
|
|
231
|
+
const { key, value } = req.body;
|
|
232
|
+
if (!key || !value) {
|
|
233
|
+
res.status(400).json({ error: "key and value are required" });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await ctx.credentialStore.set(key, value);
|
|
237
|
+
clearSkillCache();
|
|
238
|
+
res.status(201).json({ key });
|
|
239
|
+
} catch (err) {
|
|
240
|
+
res.status(500).json({ error: String(err) });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
router.get("/:key", async (req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
const value = await ctx.credentialStore.get(req.params.key);
|
|
246
|
+
if (value === null) {
|
|
247
|
+
res.status(404).json({ error: "Key not found" });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
res.json({ key: req.params.key, value });
|
|
251
|
+
} catch (err) {
|
|
252
|
+
res.status(500).json({ error: String(err) });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
router.delete("/:key", async (req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
await ctx.credentialStore.delete(req.params.key);
|
|
258
|
+
clearSkillCache();
|
|
259
|
+
res.status(204).send();
|
|
260
|
+
} catch (err) {
|
|
261
|
+
res.status(500).json({ error: String(err) });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return router;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/routes/providers.ts
|
|
268
|
+
import { Router as Router4 } from "express";
|
|
269
|
+
import { createProvider, saveConfig } from "@cortask/core";
|
|
270
|
+
var PROVIDERS = [
|
|
271
|
+
{ id: "anthropic", name: "Anthropic", defaultModel: "claude-sonnet-4-5-20250929", credKey: "provider.anthropic.apiKey" },
|
|
272
|
+
{ id: "openai", name: "OpenAI", defaultModel: "gpt-4o", credKey: "provider.openai.apiKey" },
|
|
273
|
+
{ id: "google", name: "Google", defaultModel: "gemini-2.0-flash", credKey: "provider.google.apiKey" },
|
|
274
|
+
{ id: "moonshot", name: "Moonshot", defaultModel: "moonshot-v1-8k", credKey: "provider.moonshot.apiKey" },
|
|
275
|
+
{ id: "grok", name: "Grok", defaultModel: "grok-3-latest", credKey: "provider.grok.apiKey" },
|
|
276
|
+
{ id: "openrouter", name: "OpenRouter", defaultModel: "openai/gpt-4o", credKey: "provider.openrouter.apiKey" },
|
|
277
|
+
{ id: "minimax", name: "MiniMax", defaultModel: "MiniMax-Text-01", credKey: "provider.minimax.apiKey" },
|
|
278
|
+
{ id: "ollama", name: "Ollama", defaultModel: "llama3", credKey: "provider.ollama.host" }
|
|
279
|
+
];
|
|
280
|
+
function createProviderRoutes(ctx) {
|
|
281
|
+
const router = Router4();
|
|
282
|
+
router.get("/", async (_req, res) => {
|
|
283
|
+
try {
|
|
284
|
+
const providers = await Promise.all(
|
|
285
|
+
PROVIDERS.map(async (p) => {
|
|
286
|
+
const providerConfig = ctx.config.providers[p.id];
|
|
287
|
+
const configModel = providerConfig?.model;
|
|
288
|
+
return {
|
|
289
|
+
id: p.id,
|
|
290
|
+
name: p.name,
|
|
291
|
+
defaultModel: configModel || p.defaultModel,
|
|
292
|
+
configured: await ctx.credentialStore.has(p.credKey),
|
|
293
|
+
isDefault: ctx.config.providers.default === p.id
|
|
294
|
+
};
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
res.json(providers);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
res.status(500).json({ error: String(err) });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
router.put("/default", async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const { providerId, model } = req.body;
|
|
305
|
+
if (!providerId || !model) {
|
|
306
|
+
res.status(400).json({ error: "providerId and model are required" });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
ctx.config.providers.default = providerId;
|
|
310
|
+
const key = providerId;
|
|
311
|
+
if (key in ctx.config.providers) {
|
|
312
|
+
ctx.config.providers[key] = { model };
|
|
313
|
+
}
|
|
314
|
+
await saveConfig(ctx.configPath, ctx.config);
|
|
315
|
+
res.json({ providerId, model });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
res.status(500).json({ error: String(err) });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
router.post("/:id/test", async (req, res) => {
|
|
321
|
+
try {
|
|
322
|
+
const providerId = req.params.id;
|
|
323
|
+
const provMeta = PROVIDERS.find((p) => p.id === providerId);
|
|
324
|
+
const apiKey = await ctx.credentialStore.get(
|
|
325
|
+
provMeta?.credKey ?? `provider.${providerId}.apiKey`
|
|
326
|
+
);
|
|
327
|
+
if (!apiKey) {
|
|
328
|
+
res.status(400).json({ error: "No API key configured" });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const provider = createProvider(providerId, apiKey);
|
|
332
|
+
const result = await provider.generateText({
|
|
333
|
+
model: PROVIDERS.find((p) => p.id === providerId)?.defaultModel ?? "claude-sonnet-4-5-20250929",
|
|
334
|
+
messages: [{ role: "user", content: "Say hi in 3 words" }],
|
|
335
|
+
maxTokens: 20
|
|
336
|
+
});
|
|
337
|
+
res.json({
|
|
338
|
+
success: true,
|
|
339
|
+
response: result.content,
|
|
340
|
+
usage: result.usage
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
res.status(500).json({
|
|
344
|
+
success: false,
|
|
345
|
+
error: err instanceof Error ? err.message : String(err)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return router;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/routes/skills.ts
|
|
353
|
+
import crypto from "crypto";
|
|
354
|
+
import { Router as Router5 } from "express";
|
|
355
|
+
import path2 from "path";
|
|
356
|
+
import {
|
|
357
|
+
loadSkills,
|
|
358
|
+
clearSkillCache as clearSkillCache2,
|
|
359
|
+
installSkillFromGit,
|
|
360
|
+
removeSkill,
|
|
361
|
+
getCredentialStorageKey,
|
|
362
|
+
getOAuth2StorageKeys,
|
|
363
|
+
buildSkillOAuth2AuthUrl,
|
|
364
|
+
exchangeSkillOAuth2Code,
|
|
365
|
+
revokeSkillOAuth2
|
|
366
|
+
} from "@cortask/core";
|
|
367
|
+
var oauthStates = /* @__PURE__ */ new Map();
|
|
368
|
+
var STATE_TTL_MS = 10 * 60 * 1e3;
|
|
369
|
+
setInterval(() => {
|
|
370
|
+
const now = Date.now();
|
|
371
|
+
for (const [key, val] of oauthStates) {
|
|
372
|
+
if (now - val.createdAt > STATE_TTL_MS) {
|
|
373
|
+
oauthStates.delete(key);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}, 6e4).unref();
|
|
377
|
+
function oauthHtml(title, message, script) {
|
|
378
|
+
return `<html><body><h2>${title}</h2><p>${message}</p>${script ? `<script>${script}</script>` : "<p>You can close this tab.</p>"}</body></html>`;
|
|
379
|
+
}
|
|
380
|
+
function createSkillRoutes(ctx) {
|
|
381
|
+
const router = Router5();
|
|
382
|
+
const bundledDir = ctx.bundledSkillsDir;
|
|
383
|
+
const userSkillsDir = path2.join(ctx.dataDir, "skills");
|
|
384
|
+
async function findSkillAndOAuth(name) {
|
|
385
|
+
const skills = await loadSkills(
|
|
386
|
+
bundledDir,
|
|
387
|
+
userSkillsDir,
|
|
388
|
+
ctx.config.skills.dirs,
|
|
389
|
+
ctx.credentialStore
|
|
390
|
+
);
|
|
391
|
+
const skill = skills.find((s) => s.manifest.name === name);
|
|
392
|
+
if (!skill) return null;
|
|
393
|
+
const oauth2Cred = skill.credentialSchema?.credentials.find((c) => c.type === "oauth2");
|
|
394
|
+
return { skill, oauth2Cred };
|
|
395
|
+
}
|
|
396
|
+
router.get("/", async (_req, res) => {
|
|
397
|
+
try {
|
|
398
|
+
const skills = await loadSkills(
|
|
399
|
+
bundledDir,
|
|
400
|
+
userSkillsDir,
|
|
401
|
+
ctx.config.skills.dirs,
|
|
402
|
+
ctx.credentialStore
|
|
403
|
+
);
|
|
404
|
+
const result = skills.map((s) => ({
|
|
405
|
+
name: s.manifest.name,
|
|
406
|
+
description: s.manifest.description,
|
|
407
|
+
eligible: s.eligible,
|
|
408
|
+
ineligibleReason: s.ineligibleReason,
|
|
409
|
+
source: s.source,
|
|
410
|
+
editable: s.editable,
|
|
411
|
+
hasCodeTools: s.hasCodeTools,
|
|
412
|
+
toolCount: (s.manifest.tools?.length ?? 0) + (s.hasCodeTools ? 1 : 0),
|
|
413
|
+
tags: s.manifest.metadata?.tags ?? [],
|
|
414
|
+
homepage: s.manifest.metadata?.homepage ?? null,
|
|
415
|
+
content: s.content,
|
|
416
|
+
installOptions: s.installOptions,
|
|
417
|
+
credentialSchema: s.credentialSchema,
|
|
418
|
+
credentialStatus: s.credentialStatus
|
|
419
|
+
}));
|
|
420
|
+
res.json(result);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
res.status(500).json({ error: String(err) });
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
router.post("/install", async (req, res) => {
|
|
426
|
+
try {
|
|
427
|
+
const { gitUrl } = req.body;
|
|
428
|
+
if (!gitUrl) {
|
|
429
|
+
res.status(400).json({ error: "gitUrl is required" });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const result = await installSkillFromGit(gitUrl, userSkillsDir);
|
|
433
|
+
clearSkillCache2();
|
|
434
|
+
res.status(201).json(result);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
res.status(500).json({
|
|
437
|
+
error: err instanceof Error ? err.message : String(err)
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
router.delete("/:name", async (req, res) => {
|
|
442
|
+
try {
|
|
443
|
+
await removeSkill(req.params.name, userSkillsDir);
|
|
444
|
+
clearSkillCache2();
|
|
445
|
+
res.status(204).send();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
res.status(500).json({
|
|
448
|
+
error: err instanceof Error ? err.message : String(err)
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
router.get("/:name/oauth2/authorize", async (req, res) => {
|
|
453
|
+
try {
|
|
454
|
+
const name = req.params.name;
|
|
455
|
+
const result = await findSkillAndOAuth(name);
|
|
456
|
+
if (!result || !result.oauth2Cred?.oauth) {
|
|
457
|
+
res.status(400).json({ error: "Skill not found or has no OAuth2 config" });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const credentialId = result.oauth2Cred.id;
|
|
461
|
+
const clientIdKey = getCredentialStorageKey(name, credentialId, "clientId", result.oauth2Cred.storeAs);
|
|
462
|
+
const clientId = await ctx.credentialStore.get(clientIdKey);
|
|
463
|
+
if (!clientId) {
|
|
464
|
+
res.status(400).json({ error: "Client ID not configured. Save it before authorizing." });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
468
|
+
oauthStates.set(state, { skillName: name, credentialId, createdAt: Date.now() });
|
|
469
|
+
const proto = req.headers["x-forwarded-proto"] || req.protocol || "http";
|
|
470
|
+
const host = req.headers["x-forwarded-host"] || req.headers.host || "localhost:3777";
|
|
471
|
+
const gatewayBaseUrl = `${proto}://${host}`;
|
|
472
|
+
const redirectUri = `${gatewayBaseUrl}/api/skills/${encodeURIComponent(name)}/oauth2/callback`;
|
|
473
|
+
const authorizationUrl = buildSkillOAuth2AuthUrl(
|
|
474
|
+
name,
|
|
475
|
+
result.oauth2Cred.oauth,
|
|
476
|
+
clientId,
|
|
477
|
+
redirectUri,
|
|
478
|
+
state
|
|
479
|
+
);
|
|
480
|
+
res.json({ authorizationUrl, redirectUri });
|
|
481
|
+
} catch (err) {
|
|
482
|
+
res.status(500).json({ error: String(err) });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
router.get("/:name/oauth2/redirect-uri", (req, res) => {
|
|
486
|
+
const name = req.params.name;
|
|
487
|
+
const proto = req.headers["x-forwarded-proto"] || req.protocol || "http";
|
|
488
|
+
const host = req.headers["x-forwarded-host"] || req.headers.host || "localhost:3777";
|
|
489
|
+
const gatewayBaseUrl = `${proto}://${host}`;
|
|
490
|
+
const redirectUri = `${gatewayBaseUrl}/api/skills/${encodeURIComponent(name)}/oauth2/callback`;
|
|
491
|
+
res.json({ redirectUri });
|
|
492
|
+
});
|
|
493
|
+
router.get("/:name/oauth2/callback", async (req, res) => {
|
|
494
|
+
const { code, state, error: oauthError } = req.query;
|
|
495
|
+
const name = req.params.name;
|
|
496
|
+
if (oauthError) {
|
|
497
|
+
res.status(400).send(oauthHtml("OAuth Error", oauthError));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (!code || !state) {
|
|
501
|
+
res.status(400).send(oauthHtml("Error", "Missing code or state parameter."));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const stateEntry = oauthStates.get(state);
|
|
505
|
+
if (!stateEntry || stateEntry.skillName !== name) {
|
|
506
|
+
res.status(400).send(oauthHtml("Error", "Invalid or expired OAuth state. Please try again."));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
oauthStates.delete(state);
|
|
510
|
+
if (Date.now() - stateEntry.createdAt > STATE_TTL_MS) {
|
|
511
|
+
res.status(400).send(oauthHtml("Error", "OAuth state expired. Please try again."));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const result = await findSkillAndOAuth(name);
|
|
516
|
+
if (!result || !result.oauth2Cred?.oauth) {
|
|
517
|
+
res.status(400).send(oauthHtml("Error", "Skill not found or has no OAuth2 config."));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const credentialId = result.oauth2Cred.id;
|
|
521
|
+
const clientIdKey = getCredentialStorageKey(name, credentialId, "clientId", result.oauth2Cred.storeAs);
|
|
522
|
+
const clientSecretKey = getCredentialStorageKey(name, credentialId, "clientSecret", result.oauth2Cred.storeAs);
|
|
523
|
+
const clientId = await ctx.credentialStore.get(clientIdKey);
|
|
524
|
+
const clientSecret = await ctx.credentialStore.get(clientSecretKey);
|
|
525
|
+
if (!clientId || !clientSecret) {
|
|
526
|
+
res.status(400).send(oauthHtml("Error", "OAuth client credentials not configured."));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const proto = req.headers["x-forwarded-proto"] || req.protocol || "http";
|
|
530
|
+
const host = req.headers["x-forwarded-host"] || req.headers.host || "localhost:3777";
|
|
531
|
+
const gatewayBaseUrl = `${proto}://${host}`;
|
|
532
|
+
const redirectUri = `${gatewayBaseUrl}/api/skills/${encodeURIComponent(name)}/oauth2/callback`;
|
|
533
|
+
await exchangeSkillOAuth2Code(
|
|
534
|
+
name,
|
|
535
|
+
credentialId,
|
|
536
|
+
result.oauth2Cred.oauth,
|
|
537
|
+
clientId,
|
|
538
|
+
clientSecret,
|
|
539
|
+
code,
|
|
540
|
+
redirectUri,
|
|
541
|
+
ctx.credentialStore
|
|
542
|
+
);
|
|
543
|
+
clearSkillCache2();
|
|
544
|
+
res.send(oauthHtml(
|
|
545
|
+
"Authorization Successful",
|
|
546
|
+
`Skill "${name}" has been authorized.`,
|
|
547
|
+
`if (window.opener) { window.opener.postMessage({ type: "oauth-success", skill: "${name}" }, "*"); }`
|
|
548
|
+
));
|
|
549
|
+
} catch (err) {
|
|
550
|
+
res.status(500).send(oauthHtml("Error", `Token exchange failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
router.get("/:name/oauth2/status", async (req, res) => {
|
|
554
|
+
try {
|
|
555
|
+
const name = req.params.name;
|
|
556
|
+
const result = await findSkillAndOAuth(name);
|
|
557
|
+
if (!result || !result.oauth2Cred) {
|
|
558
|
+
res.status(400).json({ error: "Skill not found or has no OAuth2 config" });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const keys = getOAuth2StorageKeys(name, result.oauth2Cred.id);
|
|
562
|
+
const hasToken = await ctx.credentialStore.has(keys.accessToken);
|
|
563
|
+
const expiresAtStr = await ctx.credentialStore.get(keys.expiresAt);
|
|
564
|
+
const hasRefresh = await ctx.credentialStore.has(keys.refreshToken);
|
|
565
|
+
let expiresAt = null;
|
|
566
|
+
let expired = false;
|
|
567
|
+
if (expiresAtStr) {
|
|
568
|
+
expiresAt = parseInt(expiresAtStr, 10);
|
|
569
|
+
expired = !isNaN(expiresAt) && Date.now() >= expiresAt;
|
|
570
|
+
}
|
|
571
|
+
res.json({ connected: hasToken, expired, expiresAt, hasRefreshToken: hasRefresh });
|
|
572
|
+
} catch (err) {
|
|
573
|
+
res.status(500).json({ error: String(err) });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
router.post("/:name/oauth2/revoke", async (req, res) => {
|
|
577
|
+
try {
|
|
578
|
+
const name = req.params.name;
|
|
579
|
+
const result = await findSkillAndOAuth(name);
|
|
580
|
+
if (!result || !result.oauth2Cred) {
|
|
581
|
+
res.status(400).json({ error: "Skill not found or has no OAuth2 config" });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
await revokeSkillOAuth2(name, result.oauth2Cred.id, ctx.credentialStore);
|
|
585
|
+
clearSkillCache2();
|
|
586
|
+
res.json({ ok: true });
|
|
587
|
+
} catch (err) {
|
|
588
|
+
res.status(500).json({ error: String(err) });
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
return router;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/routes/cron.ts
|
|
595
|
+
import { Router as Router6 } from "express";
|
|
596
|
+
function createCronRoutes(cronService) {
|
|
597
|
+
const router = Router6();
|
|
598
|
+
router.get("/", (_req, res) => {
|
|
599
|
+
try {
|
|
600
|
+
const jobs = cronService.list();
|
|
601
|
+
const result = jobs.map((job) => {
|
|
602
|
+
const state = cronService.getState(job.id);
|
|
603
|
+
return { ...job, state };
|
|
604
|
+
});
|
|
605
|
+
res.json(result);
|
|
606
|
+
} catch (err) {
|
|
607
|
+
res.status(500).json({ error: String(err) });
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
router.get("/:id", (req, res) => {
|
|
611
|
+
try {
|
|
612
|
+
const job = cronService.getJob(req.params.id);
|
|
613
|
+
if (!job) {
|
|
614
|
+
res.status(404).json({ error: "Job not found" });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const state = cronService.getState(job.id);
|
|
618
|
+
res.json({ ...job, state });
|
|
619
|
+
} catch (err) {
|
|
620
|
+
res.status(500).json({ error: String(err) });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
router.post("/", (req, res) => {
|
|
624
|
+
try {
|
|
625
|
+
const input = req.body;
|
|
626
|
+
if (!input.name || !input.schedule || !input.prompt) {
|
|
627
|
+
res.status(400).json({ error: "name, schedule, and prompt are required" });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const job = cronService.add(input);
|
|
631
|
+
res.status(201).json(job);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
res.status(500).json({
|
|
634
|
+
error: err instanceof Error ? err.message : String(err)
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
router.put("/:id", (req, res) => {
|
|
639
|
+
try {
|
|
640
|
+
const updated = cronService.update(req.params.id, req.body);
|
|
641
|
+
if (!updated) {
|
|
642
|
+
res.status(404).json({ error: "Job not found" });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
res.json(updated);
|
|
646
|
+
} catch (err) {
|
|
647
|
+
res.status(500).json({
|
|
648
|
+
error: err instanceof Error ? err.message : String(err)
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
router.delete("/:id", (req, res) => {
|
|
653
|
+
try {
|
|
654
|
+
const removed = cronService.remove(req.params.id);
|
|
655
|
+
if (!removed) {
|
|
656
|
+
res.status(404).json({ error: "Job not found" });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
res.status(204).send();
|
|
660
|
+
} catch (err) {
|
|
661
|
+
res.status(500).json({
|
|
662
|
+
error: err instanceof Error ? err.message : String(err)
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
router.post("/:id/run", async (req, res) => {
|
|
667
|
+
try {
|
|
668
|
+
await cronService.runNow(req.params.id);
|
|
669
|
+
res.json({ status: "executed" });
|
|
670
|
+
} catch (err) {
|
|
671
|
+
res.status(500).json({
|
|
672
|
+
error: err instanceof Error ? err.message : String(err)
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
return router;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/routes/channels.ts
|
|
680
|
+
import { Router as Router7 } from "express";
|
|
681
|
+
import { WhatsAppAdapter } from "@cortask/channels";
|
|
682
|
+
|
|
683
|
+
// src/ws.ts
|
|
684
|
+
import { logger } from "@cortask/core";
|
|
685
|
+
function broadcastChannelStatus(wss, status) {
|
|
686
|
+
const data = JSON.stringify({ type: "channel:status", ...status });
|
|
687
|
+
for (const client of wss.clients) {
|
|
688
|
+
if (client.readyState === client.OPEN) {
|
|
689
|
+
client.send(data);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function broadcastSessionRefresh(wss, workspaceId) {
|
|
694
|
+
const data = JSON.stringify({ type: "session:refresh", workspaceId });
|
|
695
|
+
for (const client of wss.clients) {
|
|
696
|
+
if (client.readyState === client.OPEN) {
|
|
697
|
+
client.send(data);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
var pendingPermissions = /* @__PURE__ */ new Map();
|
|
702
|
+
var pendingQuestionnaires = /* @__PURE__ */ new Map();
|
|
703
|
+
var activeRuns = /* @__PURE__ */ new Map();
|
|
704
|
+
function handleWebSocket(ws, ctx) {
|
|
705
|
+
logger.info("WebSocket client connected", "gateway");
|
|
706
|
+
ws.on("message", async (raw) => {
|
|
707
|
+
let msg;
|
|
708
|
+
try {
|
|
709
|
+
msg = JSON.parse(raw.toString());
|
|
710
|
+
} catch {
|
|
711
|
+
sendError(ws, "", "Invalid JSON");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (msg.type === "chat") {
|
|
715
|
+
await handleChat(ws, msg, ctx);
|
|
716
|
+
} else if (msg.type === "cancel") {
|
|
717
|
+
logger.info(`Cancel requested for session ${msg.sessionKey}`, "gateway");
|
|
718
|
+
const controller = activeRuns.get(msg.sessionKey);
|
|
719
|
+
if (controller) {
|
|
720
|
+
controller.abort();
|
|
721
|
+
activeRuns.delete(msg.sessionKey);
|
|
722
|
+
}
|
|
723
|
+
} else if (msg.type === "permission_response") {
|
|
724
|
+
const resolver = pendingPermissions.get(msg.requestId);
|
|
725
|
+
if (resolver) {
|
|
726
|
+
resolver(msg.approved);
|
|
727
|
+
pendingPermissions.delete(msg.requestId);
|
|
728
|
+
}
|
|
729
|
+
} else if (msg.type === "questionnaire_response") {
|
|
730
|
+
const resolver = pendingQuestionnaires.get(msg.requestId);
|
|
731
|
+
if (resolver) {
|
|
732
|
+
resolver(msg.responses);
|
|
733
|
+
pendingQuestionnaires.delete(msg.requestId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
ws.on("close", () => {
|
|
738
|
+
logger.info("WebSocket client disconnected", "gateway");
|
|
739
|
+
for (const [id, controller] of activeRuns) {
|
|
740
|
+
controller.abort();
|
|
741
|
+
activeRuns.delete(id);
|
|
742
|
+
}
|
|
743
|
+
for (const [id, resolver] of pendingPermissions) {
|
|
744
|
+
resolver(false);
|
|
745
|
+
pendingPermissions.delete(id);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function checkSpendingLimit(ctx) {
|
|
750
|
+
const { spending } = ctx.config;
|
|
751
|
+
if (!spending.enabled) return null;
|
|
752
|
+
const summary = ctx.usageStore.getSummary(spending.period);
|
|
753
|
+
if (spending.maxTokens && summary.totalTokens >= spending.maxTokens) {
|
|
754
|
+
return `Spending limit reached: ${summary.totalTokens.toLocaleString()} / ${spending.maxTokens.toLocaleString()} tokens (${spending.period})`;
|
|
755
|
+
}
|
|
756
|
+
if (spending.maxCostUsd && summary.totalCostUsd >= spending.maxCostUsd) {
|
|
757
|
+
return `Spending limit reached: $${summary.totalCostUsd.toFixed(2)} / $${spending.maxCostUsd.toFixed(2)} (${spending.period})`;
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
async function handleChat(ws, msg, ctx) {
|
|
762
|
+
const abortController = new AbortController();
|
|
763
|
+
activeRuns.set(msg.sessionKey, abortController);
|
|
764
|
+
try {
|
|
765
|
+
const limitError = checkSpendingLimit(ctx);
|
|
766
|
+
if (limitError) {
|
|
767
|
+
sendError(ws, msg.sessionKey, limitError);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const workspace = await ctx.workspaceManager.get(msg.workspaceId);
|
|
771
|
+
if (!workspace) {
|
|
772
|
+
sendError(ws, msg.sessionKey, "Workspace not found");
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const runner = await ctx.createAgentRunner(workspace.rootPath, {
|
|
776
|
+
onPermissionRequest: async (req) => {
|
|
777
|
+
return new Promise((resolve) => {
|
|
778
|
+
pendingPermissions.set(req.id, resolve);
|
|
779
|
+
send(ws, {
|
|
780
|
+
type: "permission_request",
|
|
781
|
+
requestId: req.id,
|
|
782
|
+
description: req.description,
|
|
783
|
+
details: req.details
|
|
784
|
+
});
|
|
785
|
+
setTimeout(() => {
|
|
786
|
+
if (pendingPermissions.has(req.id)) {
|
|
787
|
+
pendingPermissions.delete(req.id);
|
|
788
|
+
resolve(false);
|
|
789
|
+
}
|
|
790
|
+
}, 6e4);
|
|
791
|
+
});
|
|
792
|
+
},
|
|
793
|
+
onQuestionnaireRequest: async (req) => {
|
|
794
|
+
return new Promise((resolve) => {
|
|
795
|
+
pendingQuestionnaires.set(req.id, resolve);
|
|
796
|
+
send(ws, {
|
|
797
|
+
type: "questionnaire_request",
|
|
798
|
+
requestId: req.id,
|
|
799
|
+
data: {
|
|
800
|
+
title: req.title,
|
|
801
|
+
description: req.description,
|
|
802
|
+
questions: req.questions
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
for await (const event of runner.runStream({
|
|
809
|
+
prompt: msg.message,
|
|
810
|
+
attachments: msg.attachments,
|
|
811
|
+
sessionId: msg.sessionKey,
|
|
812
|
+
workspaceId: msg.workspaceId,
|
|
813
|
+
signal: abortController.signal
|
|
814
|
+
})) {
|
|
815
|
+
if (ws.readyState !== ws.OPEN || abortController.signal.aborted) break;
|
|
816
|
+
switch (event.type) {
|
|
817
|
+
case "thinking_delta":
|
|
818
|
+
send(ws, {
|
|
819
|
+
type: "thinking_delta",
|
|
820
|
+
sessionKey: msg.sessionKey,
|
|
821
|
+
text: event.text
|
|
822
|
+
});
|
|
823
|
+
break;
|
|
824
|
+
case "text_delta":
|
|
825
|
+
send(ws, {
|
|
826
|
+
type: "text_delta",
|
|
827
|
+
sessionKey: msg.sessionKey,
|
|
828
|
+
text: event.text
|
|
829
|
+
});
|
|
830
|
+
break;
|
|
831
|
+
case "tool_call_start":
|
|
832
|
+
send(ws, {
|
|
833
|
+
type: "tool_call_start",
|
|
834
|
+
sessionKey: msg.sessionKey,
|
|
835
|
+
toolName: event.toolName,
|
|
836
|
+
toolCallId: event.toolCallId
|
|
837
|
+
});
|
|
838
|
+
break;
|
|
839
|
+
case "tool_result":
|
|
840
|
+
send(ws, {
|
|
841
|
+
type: "tool_result",
|
|
842
|
+
sessionKey: msg.sessionKey,
|
|
843
|
+
toolCallId: event.toolCallId,
|
|
844
|
+
toolName: event.toolName,
|
|
845
|
+
toolArgs: event.toolArgs,
|
|
846
|
+
content: event.toolResult?.content,
|
|
847
|
+
isError: event.toolResult?.isError
|
|
848
|
+
});
|
|
849
|
+
break;
|
|
850
|
+
case "done":
|
|
851
|
+
if (event.usage) {
|
|
852
|
+
try {
|
|
853
|
+
const providerId = ctx.config.providers.default || "anthropic";
|
|
854
|
+
const providerConfig = ctx.config.providers[providerId];
|
|
855
|
+
const model = (typeof providerConfig === "object" && providerConfig && "model" in providerConfig ? providerConfig.model : void 0) || getDefaultModel(providerId);
|
|
856
|
+
ctx.usageStore.record(
|
|
857
|
+
providerId,
|
|
858
|
+
model,
|
|
859
|
+
event.usage.inputTokens,
|
|
860
|
+
event.usage.outputTokens
|
|
861
|
+
);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
logger.error(`Failed to record usage: ${err}`, "gateway");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
send(ws, {
|
|
867
|
+
type: "done",
|
|
868
|
+
sessionKey: msg.sessionKey,
|
|
869
|
+
usage: event.usage
|
|
870
|
+
});
|
|
871
|
+
break;
|
|
872
|
+
case "error":
|
|
873
|
+
sendError(ws, msg.sessionKey, event.error ?? "Unknown error");
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
} catch (err) {
|
|
878
|
+
if (!abortController.signal.aborted) {
|
|
879
|
+
sendError(
|
|
880
|
+
ws,
|
|
881
|
+
msg.sessionKey,
|
|
882
|
+
err instanceof Error ? err.message : String(err)
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
} finally {
|
|
886
|
+
activeRuns.delete(msg.sessionKey);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function send(ws, data) {
|
|
890
|
+
if (ws.readyState === ws.OPEN) {
|
|
891
|
+
ws.send(JSON.stringify(data));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function sendError(ws, sessionKey, error) {
|
|
895
|
+
send(ws, { type: "error", sessionKey, error });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/routes/channels.ts
|
|
899
|
+
function createChannelRoutes(channels, ctx, createChannel, wss) {
|
|
900
|
+
const router = Router7();
|
|
901
|
+
const KNOWN_CHANNELS = [
|
|
902
|
+
{ id: "telegram", name: "Telegram" },
|
|
903
|
+
{ id: "whatsapp", name: "WhatsApp" },
|
|
904
|
+
{ id: "discord", name: "Discord" }
|
|
905
|
+
];
|
|
906
|
+
router.get("/", (_req, res) => {
|
|
907
|
+
const result = KNOWN_CHANNELS.map((def) => {
|
|
908
|
+
const ch = channels.get(def.id);
|
|
909
|
+
const base = { id: def.id, name: ch?.name ?? def.name, running: ch?.isRunning() ?? false };
|
|
910
|
+
if (def.id === "whatsapp") {
|
|
911
|
+
const wa = ch;
|
|
912
|
+
return { ...base, authenticated: wa?.isAuthenticated() ?? new WhatsAppAdapter().isAuthenticated() };
|
|
913
|
+
}
|
|
914
|
+
return base;
|
|
915
|
+
});
|
|
916
|
+
res.json(result);
|
|
917
|
+
});
|
|
918
|
+
router.post("/:id/start", async (req, res) => {
|
|
919
|
+
const { id } = req.params;
|
|
920
|
+
let channel = channels.get(id);
|
|
921
|
+
if (!channel) {
|
|
922
|
+
try {
|
|
923
|
+
const created = await createChannel(id);
|
|
924
|
+
if (!created) {
|
|
925
|
+
res.status(400).json({ error: `No credentials configured for ${id}` });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
channel = created;
|
|
929
|
+
} catch (err) {
|
|
930
|
+
res.status(400).json({
|
|
931
|
+
error: err instanceof Error ? err.message : String(err)
|
|
932
|
+
});
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
channels.set(id, channel);
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
await channel.start();
|
|
939
|
+
await ctx.credentialStore.set(`channel.${id}.enabled`, "true");
|
|
940
|
+
broadcastChannelStatus(wss, { channelId: id, running: true, authenticated: true });
|
|
941
|
+
res.json({ id, name: channel.name, running: true });
|
|
942
|
+
} catch (err) {
|
|
943
|
+
res.status(500).json({
|
|
944
|
+
error: err instanceof Error ? err.message : String(err)
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
router.post("/:id/stop", async (req, res) => {
|
|
949
|
+
const channel = channels.get(req.params.id);
|
|
950
|
+
if (!channel) {
|
|
951
|
+
res.status(404).json({ error: "Channel not found" });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
await channel.stop();
|
|
956
|
+
await ctx.credentialStore.delete(`channel.${req.params.id}.enabled`);
|
|
957
|
+
broadcastChannelStatus(wss, { channelId: req.params.id, running: false, authenticated: false });
|
|
958
|
+
res.json({ id: req.params.id, name: channel.name, running: false });
|
|
959
|
+
} catch (err) {
|
|
960
|
+
res.status(500).json({
|
|
961
|
+
error: err instanceof Error ? err.message : String(err)
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
router.post("/whatsapp/qr", async (_req, res) => {
|
|
966
|
+
try {
|
|
967
|
+
let wa = channels.get("whatsapp");
|
|
968
|
+
if (!wa) {
|
|
969
|
+
const created = await createChannel("whatsapp");
|
|
970
|
+
if (!created) {
|
|
971
|
+
res.status(500).json({ error: "Failed to create WhatsApp adapter" });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
wa = created;
|
|
975
|
+
channels.set("whatsapp", wa);
|
|
976
|
+
}
|
|
977
|
+
const result = await wa.generateQR();
|
|
978
|
+
res.json(result);
|
|
979
|
+
if (result.success) {
|
|
980
|
+
const pollInterval = 2e3;
|
|
981
|
+
const maxPolls = 30;
|
|
982
|
+
let polls = 0;
|
|
983
|
+
const poller = setInterval(async () => {
|
|
984
|
+
polls++;
|
|
985
|
+
try {
|
|
986
|
+
if (wa.isAuthenticated() && !wa.isRunning()) {
|
|
987
|
+
clearInterval(poller);
|
|
988
|
+
await wa.start();
|
|
989
|
+
await ctx.credentialStore.set("channel.whatsapp.enabled", "true");
|
|
990
|
+
broadcastChannelStatus(wss, { channelId: "whatsapp", running: true, authenticated: true });
|
|
991
|
+
} else if (polls >= maxPolls) {
|
|
992
|
+
clearInterval(poller);
|
|
993
|
+
if (wa.isAuthenticated()) {
|
|
994
|
+
broadcastChannelStatus(wss, { channelId: "whatsapp", running: false, authenticated: true });
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
} catch (err) {
|
|
998
|
+
clearInterval(poller);
|
|
999
|
+
console.error("[whatsapp] Auto-start failed:", err);
|
|
1000
|
+
}
|
|
1001
|
+
}, pollInterval);
|
|
1002
|
+
}
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
res.status(500).json({
|
|
1005
|
+
success: false,
|
|
1006
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
router.post("/whatsapp/logout", async (_req, res) => {
|
|
1011
|
+
const wa = channels.get("whatsapp");
|
|
1012
|
+
if (!wa) {
|
|
1013
|
+
new WhatsAppAdapter().logout();
|
|
1014
|
+
res.json({ success: true });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
await wa.logout();
|
|
1019
|
+
await ctx.credentialStore.delete("channel.whatsapp.enabled");
|
|
1020
|
+
broadcastChannelStatus(wss, { channelId: "whatsapp", running: false, authenticated: false });
|
|
1021
|
+
res.json({ success: true });
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
res.status(500).json({
|
|
1024
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
router.get("/whatsapp/contacts", async (_req, res) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const json = await ctx.credentialStore.get("channel.whatsapp.trustedContacts");
|
|
1031
|
+
const contacts = json ? JSON.parse(json) : [];
|
|
1032
|
+
res.json(contacts);
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
router.put("/whatsapp/contacts", async (req, res) => {
|
|
1038
|
+
try {
|
|
1039
|
+
const contacts = req.body;
|
|
1040
|
+
await ctx.credentialStore.set("channel.whatsapp.trustedContacts", JSON.stringify(contacts));
|
|
1041
|
+
const wa = channels.get("whatsapp");
|
|
1042
|
+
if (wa) {
|
|
1043
|
+
wa.setTrustedContacts(contacts);
|
|
1044
|
+
}
|
|
1045
|
+
res.json(contacts);
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
return router;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/routes/artifacts.ts
|
|
1054
|
+
import { Router as Router8 } from "express";
|
|
1055
|
+
function createArtifactRoutes(artifactStore) {
|
|
1056
|
+
const router = Router8();
|
|
1057
|
+
router.get("/:id", (req, res) => {
|
|
1058
|
+
const artifact = artifactStore.get(req.params.id);
|
|
1059
|
+
if (!artifact) {
|
|
1060
|
+
res.status(404).json({ error: "Artifact not found or expired" });
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (req.query.raw !== void 0) {
|
|
1064
|
+
res.setHeader("Content-Type", artifact.mimeType);
|
|
1065
|
+
if (artifact.type === "image") {
|
|
1066
|
+
res.send(Buffer.from(artifact.content, "base64"));
|
|
1067
|
+
} else {
|
|
1068
|
+
res.send(artifact.content);
|
|
1069
|
+
}
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
res.json({
|
|
1073
|
+
id: artifact.id,
|
|
1074
|
+
type: artifact.type,
|
|
1075
|
+
title: artifact.title,
|
|
1076
|
+
content: artifact.content,
|
|
1077
|
+
mimeType: artifact.mimeType,
|
|
1078
|
+
createdAt: artifact.createdAt
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
router.get("/", (_req, res) => {
|
|
1082
|
+
const artifacts = artifactStore.list();
|
|
1083
|
+
res.json(
|
|
1084
|
+
artifacts.map((a) => ({
|
|
1085
|
+
id: a.id,
|
|
1086
|
+
type: a.type,
|
|
1087
|
+
title: a.title,
|
|
1088
|
+
mimeType: a.mimeType,
|
|
1089
|
+
createdAt: a.createdAt
|
|
1090
|
+
}))
|
|
1091
|
+
);
|
|
1092
|
+
});
|
|
1093
|
+
return router;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/routes/onboarding.ts
|
|
1097
|
+
import { Router as Router9 } from "express";
|
|
1098
|
+
import { saveConfig as saveConfig2, AVAILABLE_PROVIDERS } from "@cortask/core";
|
|
1099
|
+
function createOnboardingRoutes(ctx) {
|
|
1100
|
+
const router = Router9();
|
|
1101
|
+
router.get("/status", async (_req, res) => {
|
|
1102
|
+
try {
|
|
1103
|
+
const hasProvider = await Promise.any(
|
|
1104
|
+
AVAILABLE_PROVIDERS.flatMap((p) => [
|
|
1105
|
+
ctx.credentialStore.has(`provider.${p.id}.apiKey`),
|
|
1106
|
+
ctx.credentialStore.has(`provider.${p.id}.host`)
|
|
1107
|
+
])
|
|
1108
|
+
).catch(() => false);
|
|
1109
|
+
const workspaces = await ctx.workspaceManager.list();
|
|
1110
|
+
const hasWorkspace = workspaces.length > 0;
|
|
1111
|
+
res.json({
|
|
1112
|
+
completed: ctx.config.onboarded === true,
|
|
1113
|
+
hasProvider,
|
|
1114
|
+
hasWorkspace
|
|
1115
|
+
});
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
res.status(500).json({
|
|
1118
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
router.post("/complete", async (req, res) => {
|
|
1123
|
+
try {
|
|
1124
|
+
const { provider } = req.body;
|
|
1125
|
+
const isOllama = provider?.type === "ollama";
|
|
1126
|
+
if (!provider?.type || !provider?.apiKey && !provider?.host) {
|
|
1127
|
+
res.status(400).json({ error: "Provider configuration required" });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const credKey = isOllama ? `provider.${provider.type}.host` : `provider.${provider.type}.apiKey`;
|
|
1131
|
+
const credValue = isOllama ? provider.host : provider.apiKey;
|
|
1132
|
+
await ctx.credentialStore.set(credKey, credValue);
|
|
1133
|
+
const newConfig = {
|
|
1134
|
+
...ctx.config,
|
|
1135
|
+
onboarded: true,
|
|
1136
|
+
providers: {
|
|
1137
|
+
...ctx.config.providers,
|
|
1138
|
+
default: provider.type
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
await saveConfig2(ctx.configPath, newConfig);
|
|
1142
|
+
Object.assign(ctx.config, newConfig);
|
|
1143
|
+
const workspaces = await ctx.workspaceManager.list();
|
|
1144
|
+
if (workspaces.length === 0) {
|
|
1145
|
+
await ctx.workspaceManager.create("Default");
|
|
1146
|
+
}
|
|
1147
|
+
res.json({ success: true });
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
res.status(500).json({
|
|
1150
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
return router;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/routes/config.ts
|
|
1158
|
+
import { Router as Router10 } from "express";
|
|
1159
|
+
import { saveConfig as saveConfig3 } from "@cortask/core";
|
|
1160
|
+
function createConfigRoutes(ctx) {
|
|
1161
|
+
const router = Router10();
|
|
1162
|
+
router.get("/", (_req, res) => {
|
|
1163
|
+
try {
|
|
1164
|
+
const { agent, server, spending } = ctx.config;
|
|
1165
|
+
res.json({ agent, server, spending });
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
res.status(500).json({ error: String(err) });
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
router.put("/", async (req, res) => {
|
|
1171
|
+
try {
|
|
1172
|
+
const { agent, server, spending } = req.body;
|
|
1173
|
+
if (agent) {
|
|
1174
|
+
if (agent.maxTurns !== void 0) {
|
|
1175
|
+
const v = Math.max(1, Math.min(200, Math.round(agent.maxTurns)));
|
|
1176
|
+
ctx.config.agent.maxTurns = v;
|
|
1177
|
+
}
|
|
1178
|
+
if (agent.temperature !== void 0) {
|
|
1179
|
+
const v = Math.max(0, Math.min(2, agent.temperature));
|
|
1180
|
+
ctx.config.agent.temperature = Math.round(v * 100) / 100;
|
|
1181
|
+
}
|
|
1182
|
+
if (agent.maxTokens !== void 0) {
|
|
1183
|
+
ctx.config.agent.maxTokens = agent.maxTokens === null ? void 0 : Math.max(1, Math.round(agent.maxTokens));
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (server) {
|
|
1187
|
+
if (server.port !== void 0) {
|
|
1188
|
+
ctx.config.server.port = Math.max(1, Math.min(65535, Math.round(server.port)));
|
|
1189
|
+
}
|
|
1190
|
+
if (server.host !== void 0) {
|
|
1191
|
+
ctx.config.server.host = server.host;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (spending) {
|
|
1195
|
+
if (spending.enabled !== void 0) {
|
|
1196
|
+
ctx.config.spending.enabled = spending.enabled;
|
|
1197
|
+
}
|
|
1198
|
+
if (spending.maxTokens !== void 0) {
|
|
1199
|
+
ctx.config.spending.maxTokens = spending.maxTokens === null ? void 0 : Math.max(0, Math.round(spending.maxTokens));
|
|
1200
|
+
}
|
|
1201
|
+
if (spending.maxCostUsd !== void 0) {
|
|
1202
|
+
ctx.config.spending.maxCostUsd = spending.maxCostUsd === null ? void 0 : Math.max(0, spending.maxCostUsd);
|
|
1203
|
+
}
|
|
1204
|
+
if (spending.period !== void 0) {
|
|
1205
|
+
ctx.config.spending.period = spending.period;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
await saveConfig3(ctx.configPath, ctx.config);
|
|
1209
|
+
res.json({
|
|
1210
|
+
agent: ctx.config.agent,
|
|
1211
|
+
server: ctx.config.server,
|
|
1212
|
+
spending: ctx.config.spending
|
|
1213
|
+
});
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
res.status(500).json({ error: String(err) });
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
return router;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/routes/usage.ts
|
|
1222
|
+
import { Router as Router11 } from "express";
|
|
1223
|
+
function createUsageRoutes(usageStore) {
|
|
1224
|
+
const router = Router11();
|
|
1225
|
+
router.get("/", (req, res) => {
|
|
1226
|
+
try {
|
|
1227
|
+
const period = req.query.period || "monthly";
|
|
1228
|
+
if (!["daily", "weekly", "monthly"].includes(period)) {
|
|
1229
|
+
res.status(400).json({ error: "Invalid period. Use daily, weekly, or monthly." });
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const summary = usageStore.getSummary(period);
|
|
1233
|
+
res.json(summary);
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
res.status(500).json({ error: String(err) });
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
router.get("/history", (req, res) => {
|
|
1239
|
+
try {
|
|
1240
|
+
const days = Math.min(365, Math.max(1, parseInt(req.query.days) || 30));
|
|
1241
|
+
const history = usageStore.getHistory(days);
|
|
1242
|
+
res.json(history);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
res.status(500).json({ error: String(err) });
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
return router;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/routes/models.ts
|
|
1251
|
+
import { Router as Router12 } from "express";
|
|
1252
|
+
import { createProvider as createProvider2, getModelDefinitions } from "@cortask/core";
|
|
1253
|
+
function createModelRoutes(ctx) {
|
|
1254
|
+
const router = Router12();
|
|
1255
|
+
router.get("/:providerId/available", async (req, res) => {
|
|
1256
|
+
try {
|
|
1257
|
+
const providerId = req.params.providerId;
|
|
1258
|
+
if (providerId === "openrouter" || providerId === "ollama") {
|
|
1259
|
+
const credKey = providerId === "ollama" ? "provider.ollama.host" : `provider.${providerId}.apiKey`;
|
|
1260
|
+
const credential = await ctx.credentialStore.get(credKey);
|
|
1261
|
+
if (!credential) {
|
|
1262
|
+
res.status(400).json({ error: `No credentials configured for ${providerId}` });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const provider = createProvider2(providerId, credential);
|
|
1266
|
+
const models2 = await provider.listModels();
|
|
1267
|
+
res.json(models2);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const models = getModelDefinitions(providerId);
|
|
1271
|
+
res.json(models);
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
router.get("/enabled", async (_req, res) => {
|
|
1277
|
+
try {
|
|
1278
|
+
const provider = _req.query.provider;
|
|
1279
|
+
const models = ctx.modelStore.list(provider);
|
|
1280
|
+
res.json(models);
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
res.status(500).json({ error: String(err) });
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
router.post("/enable", async (req, res) => {
|
|
1286
|
+
try {
|
|
1287
|
+
const { provider, modelId, label, inputPricePer1m, outputPricePer1m } = req.body;
|
|
1288
|
+
if (!provider || !modelId || !label) {
|
|
1289
|
+
res.status(400).json({ error: "provider, modelId, and label are required" });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const model = ctx.modelStore.enable(
|
|
1293
|
+
provider,
|
|
1294
|
+
modelId,
|
|
1295
|
+
label,
|
|
1296
|
+
inputPricePer1m ?? 0,
|
|
1297
|
+
outputPricePer1m ?? 0
|
|
1298
|
+
);
|
|
1299
|
+
res.json(model);
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
res.status(500).json({ error: String(err) });
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
router.delete("/disable", async (req, res) => {
|
|
1305
|
+
try {
|
|
1306
|
+
const { provider, modelId } = req.body;
|
|
1307
|
+
if (!provider || !modelId) {
|
|
1308
|
+
res.status(400).json({ error: "provider and modelId are required" });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
ctx.modelStore.disable(provider, modelId);
|
|
1312
|
+
res.json({ success: true });
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
res.status(500).json({ error: String(err) });
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
return router;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/routes/templates.ts
|
|
1321
|
+
import { Router as Router13 } from "express";
|
|
1322
|
+
function createTemplateRoutes(templateStore) {
|
|
1323
|
+
const router = Router13();
|
|
1324
|
+
router.get("/", (_req, res) => {
|
|
1325
|
+
try {
|
|
1326
|
+
const templates = templateStore.list();
|
|
1327
|
+
res.json(templates);
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
res.status(500).json({ error: String(err) });
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
router.post("/", (req, res) => {
|
|
1333
|
+
try {
|
|
1334
|
+
const { name, content, category } = req.body;
|
|
1335
|
+
if (!name || !content) {
|
|
1336
|
+
res.status(400).json({ error: "name and content are required" });
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const template = templateStore.create(name, content, category);
|
|
1340
|
+
res.status(201).json(template);
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
res.status(500).json({ error: String(err) });
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
router.put("/:id", (req, res) => {
|
|
1346
|
+
try {
|
|
1347
|
+
const updated = templateStore.update(req.params.id, req.body);
|
|
1348
|
+
if (!updated) {
|
|
1349
|
+
res.status(404).json({ error: "Template not found" });
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
res.json(updated);
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
res.status(500).json({ error: String(err) });
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
router.delete("/:id", (req, res) => {
|
|
1358
|
+
try {
|
|
1359
|
+
templateStore.delete(req.params.id);
|
|
1360
|
+
res.status(204).send();
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
res.status(500).json({ error: String(err) });
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
return router;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/server.ts
|
|
1369
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1370
|
+
var __dirname = path3.dirname(__filename);
|
|
1371
|
+
var sessionStoreCache = /* @__PURE__ */ new Map();
|
|
1372
|
+
function getSessionStore(workspacePath) {
|
|
1373
|
+
const dbPath = path3.join(workspacePath, ".cortask", "sessions.db");
|
|
1374
|
+
let store = sessionStoreCache.get(dbPath);
|
|
1375
|
+
if (!store) {
|
|
1376
|
+
store = new SessionStore(dbPath);
|
|
1377
|
+
sessionStoreCache.set(dbPath, store);
|
|
1378
|
+
}
|
|
1379
|
+
return store;
|
|
1380
|
+
}
|
|
1381
|
+
async function startServer(port, host) {
|
|
1382
|
+
const dataDir = getDataDir();
|
|
1383
|
+
const configPath = path3.join(dataDir, "config.yaml");
|
|
1384
|
+
const config = await loadConfig(configPath);
|
|
1385
|
+
logger2.init(path3.join(dataDir, "logs"));
|
|
1386
|
+
logger2.info("Starting Cortask gateway", "gateway");
|
|
1387
|
+
const dbPath = path3.join(dataDir, "cortask.db");
|
|
1388
|
+
const workspaceManager = new WorkspaceManager(dbPath);
|
|
1389
|
+
try {
|
|
1390
|
+
const workspaces = await workspaceManager.list();
|
|
1391
|
+
migrateAllWorkspaces(workspaces);
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
logger2.error(
|
|
1394
|
+
`Database migration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1395
|
+
"gateway"
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const secret = await getOrCreateSecret(dataDir);
|
|
1399
|
+
const credentialStore = new EncryptedCredentialStore(
|
|
1400
|
+
path3.join(dataDir, "credentials.enc.json"),
|
|
1401
|
+
secret
|
|
1402
|
+
);
|
|
1403
|
+
const cronService = new CronService(dbPath);
|
|
1404
|
+
const modelStore = new ModelStore(dbPath);
|
|
1405
|
+
const templateStore = new TemplateStore(dbPath);
|
|
1406
|
+
const usageStore = new UsageStore(dbPath, modelStore);
|
|
1407
|
+
const artifactStore = new ArtifactStore();
|
|
1408
|
+
const channels = /* @__PURE__ */ new Map();
|
|
1409
|
+
let wss;
|
|
1410
|
+
function wireMessageHandler(channel) {
|
|
1411
|
+
const channelType = channel.id;
|
|
1412
|
+
channel.onMessage(async (msg) => {
|
|
1413
|
+
try {
|
|
1414
|
+
if (config.spending.enabled) {
|
|
1415
|
+
const summary = usageStore.getSummary(config.spending.period);
|
|
1416
|
+
if (config.spending.maxTokens && summary.totalTokens >= config.spending.maxTokens) {
|
|
1417
|
+
return "Spending limit reached. Please increase or disable the limit.";
|
|
1418
|
+
}
|
|
1419
|
+
if (config.spending.maxCostUsd && summary.totalCostUsd >= config.spending.maxCostUsd) {
|
|
1420
|
+
return "Spending limit reached. Please increase or disable the limit.";
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const chatKey = `${channel.id}-${msg.chatId}`;
|
|
1424
|
+
const workspaces = await workspaceManager.list();
|
|
1425
|
+
if (workspaces.length === 0) return "No workspace configured.";
|
|
1426
|
+
const mappedWorkspaceId = workspaceManager.getChannelWorkspace(chatKey);
|
|
1427
|
+
const workspace = mappedWorkspaceId ? await workspaceManager.get(mappedWorkspaceId) ?? workspaces[0] : workspaces[0];
|
|
1428
|
+
const runner = await createAgentRunner(workspace.rootPath, void 0, {
|
|
1429
|
+
channelType,
|
|
1430
|
+
chatKey,
|
|
1431
|
+
chatId: msg.chatId
|
|
1432
|
+
});
|
|
1433
|
+
const result = await runner.run({
|
|
1434
|
+
prompt: msg.text,
|
|
1435
|
+
sessionId: chatKey
|
|
1436
|
+
});
|
|
1437
|
+
if (wss) broadcastSessionRefresh(wss, workspace.id);
|
|
1438
|
+
return result.response;
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
const bundledSkillsDir = process.env.CORTASK_SKILLS_DIR ?? path3.join(path3.resolve(__dirname, "..", "..", ".."), "skills");
|
|
1445
|
+
const userSkillsDir = path3.join(dataDir, "skills");
|
|
1446
|
+
const uiOnlyTools = /* @__PURE__ */ new Set(["questionnaire", "artifact", "show_file"]);
|
|
1447
|
+
async function createAgentRunner(workspacePath, options, channelCtx) {
|
|
1448
|
+
const providerId = config.providers.default || "anthropic";
|
|
1449
|
+
const providerConfig = config.providers[providerId];
|
|
1450
|
+
const model = providerConfig?.model || getDefaultModel(providerId);
|
|
1451
|
+
const credKey = providerId === "ollama" ? "provider.ollama.host" : `provider.${providerId}.apiKey`;
|
|
1452
|
+
const apiKey = await credentialStore.get(credKey);
|
|
1453
|
+
if (!apiKey) {
|
|
1454
|
+
throw new Error(
|
|
1455
|
+
`No credentials found for provider "${providerId}". Set it in Settings.`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
const provider = createProvider3(providerId, apiKey);
|
|
1459
|
+
const sessionStore = getSessionStore(workspacePath);
|
|
1460
|
+
const allSkills = await loadSkills2(
|
|
1461
|
+
bundledSkillsDir,
|
|
1462
|
+
userSkillsDir,
|
|
1463
|
+
config.skills.dirs,
|
|
1464
|
+
credentialStore
|
|
1465
|
+
);
|
|
1466
|
+
const eligible = getEligibleSkills(allSkills);
|
|
1467
|
+
const skillRegistry = await buildSkillTools(eligible, credentialStore);
|
|
1468
|
+
const skillToolHandlers = skillRegistry.toolDefs.map((def) => ({
|
|
1469
|
+
definition: def,
|
|
1470
|
+
execute: async (args, _context) => {
|
|
1471
|
+
const handler = skillRegistry.handlers.get(def.name);
|
|
1472
|
+
if (!handler) {
|
|
1473
|
+
return { toolCallId: "", content: `No handler for skill tool: ${def.name}`, isError: true };
|
|
1474
|
+
}
|
|
1475
|
+
const result = await handler(args);
|
|
1476
|
+
return result;
|
|
1477
|
+
}
|
|
1478
|
+
}));
|
|
1479
|
+
const skillPrompts = eligible.map((s) => `- **${s.manifest.name}**: ${s.manifest.description}`);
|
|
1480
|
+
const channelTools = channelCtx ? [createSwitchWorkspaceTool(workspaceManager, channelCtx.chatKey)] : [];
|
|
1481
|
+
const runner = new AgentRunner({
|
|
1482
|
+
config: {
|
|
1483
|
+
provider,
|
|
1484
|
+
model,
|
|
1485
|
+
maxTurns: config.agent.maxTurns,
|
|
1486
|
+
temperature: config.agent.temperature,
|
|
1487
|
+
maxTokens: config.agent.maxTokens
|
|
1488
|
+
},
|
|
1489
|
+
tools: [
|
|
1490
|
+
...channelCtx ? builtinTools.filter((t) => !uiOnlyTools.has(t.definition.name)) : builtinTools,
|
|
1491
|
+
createCronTool(cronService),
|
|
1492
|
+
...channelCtx ? [] : [createArtifactTool(artifactStore)],
|
|
1493
|
+
createBrowserTool(artifactStore),
|
|
1494
|
+
createSubagentTool(),
|
|
1495
|
+
...channelTools,
|
|
1496
|
+
...skillToolHandlers
|
|
1497
|
+
],
|
|
1498
|
+
channel: channelCtx ? { type: channelCtx.channelType, chatId: channelCtx.chatId } : void 0,
|
|
1499
|
+
getWorkspacePath: () => workspacePath,
|
|
1500
|
+
getDataDir: () => dataDir,
|
|
1501
|
+
getMemoryContent: () => workspaceManager.readMemory(workspacePath),
|
|
1502
|
+
getGlobalMemoryContent: () => workspaceManager.readGlobalMemory(dataDir),
|
|
1503
|
+
getSkillPrompts: () => skillPrompts,
|
|
1504
|
+
getSessionMessages: async (sessionId) => {
|
|
1505
|
+
return sessionStore.getMessages(sessionId);
|
|
1506
|
+
},
|
|
1507
|
+
saveSessionMessages: async (sessionId, messages) => {
|
|
1508
|
+
sessionStore.saveMessages(sessionId, messages, channelCtx?.channelType);
|
|
1509
|
+
const session = sessionStore.getSession(sessionId);
|
|
1510
|
+
if (session && session.title === "New Chat") {
|
|
1511
|
+
const firstUserMsg = messages.find((m) => m.role === "user");
|
|
1512
|
+
if (firstUserMsg) {
|
|
1513
|
+
const text = typeof firstUserMsg.content === "string" ? firstUserMsg.content : firstUserMsg.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(" ");
|
|
1514
|
+
if (text) {
|
|
1515
|
+
sessionStore.updateTitle(
|
|
1516
|
+
sessionId,
|
|
1517
|
+
text.length > 80 ? text.slice(0, 80) + "\u2026" : text
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
onPermissionRequest: options?.onPermissionRequest,
|
|
1524
|
+
onQuestionnaireRequest: options?.onQuestionnaireRequest
|
|
1525
|
+
});
|
|
1526
|
+
setSubagentRunner(runner);
|
|
1527
|
+
return runner;
|
|
1528
|
+
}
|
|
1529
|
+
const ctx = {
|
|
1530
|
+
config,
|
|
1531
|
+
configPath,
|
|
1532
|
+
dataDir,
|
|
1533
|
+
bundledSkillsDir,
|
|
1534
|
+
workspaceManager,
|
|
1535
|
+
credentialStore,
|
|
1536
|
+
usageStore,
|
|
1537
|
+
modelStore,
|
|
1538
|
+
getSessionStore,
|
|
1539
|
+
createAgentRunner
|
|
1540
|
+
};
|
|
1541
|
+
const app = express();
|
|
1542
|
+
app.use(cors());
|
|
1543
|
+
app.use(express.json({ limit: "10mb" }));
|
|
1544
|
+
const server = createServer(app);
|
|
1545
|
+
wss = new WebSocketServer({ server, path: "/ws" });
|
|
1546
|
+
wss.on("connection", (ws) => {
|
|
1547
|
+
handleWebSocket(ws, ctx);
|
|
1548
|
+
});
|
|
1549
|
+
app.use("/api/onboarding", createOnboardingRoutes(ctx));
|
|
1550
|
+
app.use("/api/workspaces", createWorkspaceRoutes(ctx));
|
|
1551
|
+
app.use("/api/sessions", createSessionRoutes(ctx));
|
|
1552
|
+
app.use("/api/credentials", createCredentialRoutes(ctx));
|
|
1553
|
+
app.use("/api/providers", createProviderRoutes(ctx));
|
|
1554
|
+
app.use("/api/config", createConfigRoutes(ctx));
|
|
1555
|
+
app.use("/api/skills", createSkillRoutes(ctx));
|
|
1556
|
+
app.use("/api/cron", createCronRoutes(cronService));
|
|
1557
|
+
app.use("/api/usage", createUsageRoutes(usageStore));
|
|
1558
|
+
app.use("/api/models", createModelRoutes(ctx));
|
|
1559
|
+
app.use("/api/templates", createTemplateRoutes(templateStore));
|
|
1560
|
+
async function createChannelAdapter(id) {
|
|
1561
|
+
if (id === "telegram") {
|
|
1562
|
+
const botToken = await credentialStore.get("channel.telegram.botToken");
|
|
1563
|
+
if (!botToken) return null;
|
|
1564
|
+
const adapter = new TelegramAdapter({
|
|
1565
|
+
botToken,
|
|
1566
|
+
allowedUsers: config.channels?.telegram?.allowedUsers ?? []
|
|
1567
|
+
});
|
|
1568
|
+
wireMessageHandler(adapter);
|
|
1569
|
+
return adapter;
|
|
1570
|
+
}
|
|
1571
|
+
if (id === "discord") {
|
|
1572
|
+
const botToken = await credentialStore.get("channel.discord.botToken");
|
|
1573
|
+
if (!botToken) return null;
|
|
1574
|
+
const adapter = new DiscordAdapter({ botToken });
|
|
1575
|
+
wireMessageHandler(adapter);
|
|
1576
|
+
return adapter;
|
|
1577
|
+
}
|
|
1578
|
+
if (id === "whatsapp") {
|
|
1579
|
+
const contactsJson = await credentialStore.get("channel.whatsapp.trustedContacts");
|
|
1580
|
+
const trustedContacts = contactsJson ? JSON.parse(contactsJson) : [];
|
|
1581
|
+
const adapter = new WhatsAppAdapter2({ trustedContacts });
|
|
1582
|
+
wireMessageHandler(adapter);
|
|
1583
|
+
return adapter;
|
|
1584
|
+
}
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
app.use("/api/channels", createChannelRoutes(channels, ctx, createChannelAdapter, wss));
|
|
1588
|
+
app.use("/api/artifacts", createArtifactRoutes(artifactStore));
|
|
1589
|
+
app.get("/api/health", (_req, res) => {
|
|
1590
|
+
res.json({ status: "ok", version: "0.1.0" });
|
|
1591
|
+
});
|
|
1592
|
+
const resourcesPath = process.resourcesPath;
|
|
1593
|
+
const uiDistCandidates = [
|
|
1594
|
+
path3.resolve("../ui/dist"),
|
|
1595
|
+
// monorepo dev
|
|
1596
|
+
path3.resolve(__dirname, "../../ui/dist"),
|
|
1597
|
+
// relative to gateway dist
|
|
1598
|
+
...resourcesPath ? [path3.resolve(resourcesPath, "ui")] : []
|
|
1599
|
+
// electron packaged
|
|
1600
|
+
];
|
|
1601
|
+
for (const uiDir of uiDistCandidates) {
|
|
1602
|
+
if (fs.existsSync(path3.join(uiDir, "index.html"))) {
|
|
1603
|
+
app.use(express.static(uiDir));
|
|
1604
|
+
app.get("*", (_req, res, next) => {
|
|
1605
|
+
if (_req.path.startsWith("/api/") || _req.path.startsWith("/ws")) {
|
|
1606
|
+
return next();
|
|
1607
|
+
}
|
|
1608
|
+
res.sendFile(path3.join(uiDir, "index.html"));
|
|
1609
|
+
});
|
|
1610
|
+
logger2.info(`Serving UI from ${uiDir}`, "gateway");
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const envPort = process.env.CORTASK_PORT ? parseInt(process.env.CORTASK_PORT, 10) : void 0;
|
|
1615
|
+
const envHost = process.env.CORTASK_HOST;
|
|
1616
|
+
const finalPort = port ?? envPort ?? config.server.port;
|
|
1617
|
+
const finalHost = host ?? envHost ?? config.server.host;
|
|
1618
|
+
cronService.setExecutor(async (job) => {
|
|
1619
|
+
if (config.spending.enabled) {
|
|
1620
|
+
const summary = usageStore.getSummary(config.spending.period);
|
|
1621
|
+
if (config.spending.maxTokens && summary.totalTokens >= config.spending.maxTokens) {
|
|
1622
|
+
throw new Error(`Spending limit reached: ${summary.totalTokens.toLocaleString()} / ${config.spending.maxTokens.toLocaleString()} tokens`);
|
|
1623
|
+
}
|
|
1624
|
+
if (config.spending.maxCostUsd && summary.totalCostUsd >= config.spending.maxCostUsd) {
|
|
1625
|
+
throw new Error(`Spending limit reached: $${summary.totalCostUsd.toFixed(2)} / $${config.spending.maxCostUsd.toFixed(2)}`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const workspaces = await workspaceManager.list();
|
|
1629
|
+
let workspacePath;
|
|
1630
|
+
if (job.workspaceId) {
|
|
1631
|
+
const ws = await workspaceManager.get(job.workspaceId);
|
|
1632
|
+
workspacePath = ws?.rootPath ?? workspaces[0]?.rootPath ?? dataDir;
|
|
1633
|
+
} else {
|
|
1634
|
+
workspacePath = workspaces[0]?.rootPath ?? dataDir;
|
|
1635
|
+
}
|
|
1636
|
+
const runner = await createAgentRunner(workspacePath);
|
|
1637
|
+
const result = await runner.run({ prompt: job.prompt });
|
|
1638
|
+
if (job.delivery.channel && job.delivery.target) {
|
|
1639
|
+
const channel = channels.get(job.delivery.channel);
|
|
1640
|
+
if (!channel) {
|
|
1641
|
+
logger2.warn(`Cron job "${job.name}" delivery failed: channel "${job.delivery.channel}" not found`, "cron");
|
|
1642
|
+
} else if (!channel.isRunning()) {
|
|
1643
|
+
logger2.warn(`Cron job "${job.name}" delivery failed: channel "${job.delivery.channel}" is not running`, "cron");
|
|
1644
|
+
} else {
|
|
1645
|
+
await channel.sendMessage(job.delivery.target, result.response);
|
|
1646
|
+
}
|
|
1647
|
+
} else if (!job.delivery.channel && !job.delivery.target) {
|
|
1648
|
+
logger2.debug(`Cron job "${job.name}" has no delivery channel configured`, "cron");
|
|
1649
|
+
}
|
|
1650
|
+
return result.response;
|
|
1651
|
+
});
|
|
1652
|
+
cronService.start();
|
|
1653
|
+
await new Promise((resolve, reject) => {
|
|
1654
|
+
server.once("error", (err) => {
|
|
1655
|
+
reject(err);
|
|
1656
|
+
});
|
|
1657
|
+
server.listen(finalPort, finalHost, () => {
|
|
1658
|
+
logger2.info(
|
|
1659
|
+
`Gateway running on http://${finalHost}:${finalPort}`,
|
|
1660
|
+
"gateway"
|
|
1661
|
+
);
|
|
1662
|
+
console.log(`Cortask gateway running on http://${finalHost}:${finalPort}`);
|
|
1663
|
+
const knownChannelIds = ["telegram", "discord", "whatsapp"];
|
|
1664
|
+
for (const id of knownChannelIds) {
|
|
1665
|
+
credentialStore.get(`channel.${id}.enabled`).then(async (enabled) => {
|
|
1666
|
+
if (enabled !== "true") return;
|
|
1667
|
+
try {
|
|
1668
|
+
let channel = channels.get(id);
|
|
1669
|
+
if (!channel) {
|
|
1670
|
+
const created = await createChannelAdapter(id);
|
|
1671
|
+
if (!created) return;
|
|
1672
|
+
channel = created;
|
|
1673
|
+
channels.set(id, channel);
|
|
1674
|
+
}
|
|
1675
|
+
await channel.start();
|
|
1676
|
+
logger2.info(`Channel "${id}" auto-started`, "gateway");
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
logger2.error(`Failed to auto-start channel "${id}": ${err}`, "gateway");
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
setInterval(() => {
|
|
1683
|
+
cleanupSubagentRecords(30 * 60 * 1e3);
|
|
1684
|
+
}, 10 * 60 * 1e3);
|
|
1685
|
+
resolve();
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
return { server, wss, ctx };
|
|
1689
|
+
}
|
|
1690
|
+
function getDefaultModel(providerId) {
|
|
1691
|
+
switch (providerId) {
|
|
1692
|
+
case "anthropic":
|
|
1693
|
+
return "claude-sonnet-4-5-20250929";
|
|
1694
|
+
case "openai":
|
|
1695
|
+
return "gpt-4o";
|
|
1696
|
+
case "google":
|
|
1697
|
+
return "gemini-2.0-flash";
|
|
1698
|
+
case "moonshot":
|
|
1699
|
+
return "moonshot-v1-8k";
|
|
1700
|
+
case "grok":
|
|
1701
|
+
return "grok-3-latest";
|
|
1702
|
+
case "openrouter":
|
|
1703
|
+
return "openai/gpt-4o";
|
|
1704
|
+
case "minimax":
|
|
1705
|
+
return "MiniMax-Text-01";
|
|
1706
|
+
default:
|
|
1707
|
+
return "claude-sonnet-4-5-20250929";
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/index.ts
|
|
1712
|
+
var isMainModule = typeof process !== "undefined" && process.argv[1]?.endsWith("index.js");
|
|
1713
|
+
if (isMainModule) {
|
|
1714
|
+
startServer().catch((err) => {
|
|
1715
|
+
console.error("Failed to start gateway:", err);
|
|
1716
|
+
process.exit(1);
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
export {
|
|
1720
|
+
startServer
|
|
1721
|
+
};
|
|
1722
|
+
//# sourceMappingURL=index.js.map
|