opc-agent 3.0.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +404 -74
  2. package/README.zh-CN.md +82 -0
  3. package/dist/channels/dingtalk.d.ts +17 -0
  4. package/dist/channels/dingtalk.js +38 -0
  5. package/dist/channels/googlechat.d.ts +14 -0
  6. package/dist/channels/googlechat.js +37 -0
  7. package/dist/channels/imessage.d.ts +13 -0
  8. package/dist/channels/imessage.js +28 -0
  9. package/dist/channels/irc.d.ts +20 -0
  10. package/dist/channels/irc.js +71 -0
  11. package/dist/channels/line.d.ts +14 -0
  12. package/dist/channels/line.js +28 -0
  13. package/dist/channels/matrix.d.ts +15 -0
  14. package/dist/channels/matrix.js +28 -0
  15. package/dist/channels/mattermost.d.ts +18 -0
  16. package/dist/channels/mattermost.js +49 -0
  17. package/dist/channels/msteams.d.ts +14 -0
  18. package/dist/channels/msteams.js +28 -0
  19. package/dist/channels/nostr.d.ts +14 -0
  20. package/dist/channels/nostr.js +28 -0
  21. package/dist/channels/qq.d.ts +15 -0
  22. package/dist/channels/qq.js +28 -0
  23. package/dist/channels/signal.d.ts +14 -0
  24. package/dist/channels/signal.js +28 -0
  25. package/dist/channels/sms.d.ts +15 -0
  26. package/dist/channels/sms.js +28 -0
  27. package/dist/channels/twitch.d.ts +17 -0
  28. package/dist/channels/twitch.js +59 -0
  29. package/dist/channels/voice-call.d.ts +27 -0
  30. package/dist/channels/voice-call.js +82 -0
  31. package/dist/channels/whatsapp.d.ts +14 -0
  32. package/dist/channels/whatsapp.js +28 -0
  33. package/dist/cli/chat.d.ts +2 -0
  34. package/dist/cli/chat.js +134 -0
  35. package/dist/cli/setup.d.ts +4 -0
  36. package/dist/cli/setup.js +303 -0
  37. package/dist/cli.js +142 -6
  38. package/dist/core/api-server.d.ts +25 -0
  39. package/dist/core/api-server.js +286 -0
  40. package/dist/core/audio.d.ts +50 -0
  41. package/dist/core/audio.js +68 -0
  42. package/dist/core/context-discovery.d.ts +16 -0
  43. package/dist/core/context-discovery.js +107 -0
  44. package/dist/core/context-refs.d.ts +29 -0
  45. package/dist/core/context-refs.js +162 -0
  46. package/dist/core/gateway.d.ts +53 -0
  47. package/dist/core/gateway.js +80 -0
  48. package/dist/core/heartbeat.d.ts +19 -0
  49. package/dist/core/heartbeat.js +50 -0
  50. package/dist/core/hooks.d.ts +28 -0
  51. package/dist/core/hooks.js +82 -0
  52. package/dist/core/ide-bridge.d.ts +53 -0
  53. package/dist/core/ide-bridge.js +97 -0
  54. package/dist/core/node-network.d.ts +23 -0
  55. package/dist/core/node-network.js +77 -0
  56. package/dist/core/profiles.d.ts +27 -0
  57. package/dist/core/profiles.js +131 -0
  58. package/dist/core/sandbox.d.ts +25 -0
  59. package/dist/core/sandbox.js +84 -1
  60. package/dist/core/session-manager.d.ts +33 -0
  61. package/dist/core/session-manager.js +157 -0
  62. package/dist/core/vision.d.ts +45 -0
  63. package/dist/core/vision.js +177 -0
  64. package/dist/hub/brain-seed.d.ts +14 -0
  65. package/dist/hub/brain-seed.js +77 -0
  66. package/dist/hub/client.d.ts +25 -0
  67. package/dist/hub/client.js +44 -0
  68. package/dist/index.d.ts +66 -1
  69. package/dist/index.js +95 -3
  70. package/dist/memory/context-compressor.d.ts +43 -0
  71. package/dist/memory/context-compressor.js +167 -0
  72. package/dist/memory/index.d.ts +4 -0
  73. package/dist/memory/index.js +5 -1
  74. package/dist/memory/user-profiler.d.ts +50 -0
  75. package/dist/memory/user-profiler.js +201 -0
  76. package/dist/providers/index.d.ts +1 -1
  77. package/dist/providers/index.js +54 -1
  78. package/dist/scheduler/cron-engine.d.ts +41 -0
  79. package/dist/scheduler/cron-engine.js +200 -0
  80. package/dist/scheduler/index.d.ts +3 -0
  81. package/dist/scheduler/index.js +7 -0
  82. package/dist/schema/oad.d.ts +12 -12
  83. package/dist/security/approvals.d.ts +53 -0
  84. package/dist/security/approvals.js +115 -0
  85. package/dist/security/elevated.d.ts +41 -0
  86. package/dist/security/elevated.js +89 -0
  87. package/dist/security/index.d.ts +6 -0
  88. package/dist/security/index.js +7 -1
  89. package/dist/security/secrets.d.ts +34 -0
  90. package/dist/security/secrets.js +115 -0
  91. package/dist/skills/builtin/index.d.ts +6 -0
  92. package/dist/skills/builtin/index.js +402 -0
  93. package/dist/skills/marketplace.d.ts +30 -0
  94. package/dist/skills/marketplace.js +142 -0
  95. package/dist/skills/types.d.ts +34 -0
  96. package/dist/skills/types.js +16 -0
  97. package/dist/studio/server.d.ts +25 -0
  98. package/dist/studio/server.js +780 -0
  99. package/dist/studio/templates-data.d.ts +21 -0
  100. package/dist/studio/templates-data.js +148 -0
  101. package/dist/studio-ui/index.html +2502 -1073
  102. package/dist/tools/builtin/browser.d.ts +47 -0
  103. package/dist/tools/builtin/browser.js +284 -0
  104. package/dist/tools/builtin/home-assistant.d.ts +12 -0
  105. package/dist/tools/builtin/home-assistant.js +126 -0
  106. package/dist/tools/builtin/index.d.ts +7 -1
  107. package/dist/tools/builtin/index.js +23 -2
  108. package/dist/tools/builtin/rl-tools.d.ts +13 -0
  109. package/dist/tools/builtin/rl-tools.js +228 -0
  110. package/dist/tools/builtin/vision.d.ts +6 -0
  111. package/dist/tools/builtin/vision.js +61 -0
  112. package/dist/tools/builtin/web-search.d.ts +9 -0
  113. package/dist/tools/builtin/web-search.js +150 -0
  114. package/dist/tools/document-processor.d.ts +39 -0
  115. package/dist/tools/document-processor.js +188 -0
  116. package/dist/tools/image-generator.d.ts +42 -0
  117. package/dist/tools/image-generator.js +136 -0
  118. package/dist/tools/web-scraper.d.ts +20 -0
  119. package/dist/tools/web-scraper.js +148 -0
  120. package/dist/tools/web-search.d.ts +51 -0
  121. package/dist/tools/web-search.js +152 -0
  122. package/install.ps1 +154 -0
  123. package/install.sh +164 -0
  124. package/package.json +63 -52
  125. package/src/channels/dingtalk.ts +46 -0
  126. package/src/channels/googlechat.ts +42 -0
  127. package/src/channels/imessage.ts +32 -0
  128. package/src/channels/irc.ts +82 -0
  129. package/src/channels/line.ts +33 -0
  130. package/src/channels/matrix.ts +34 -0
  131. package/src/channels/mattermost.ts +57 -0
  132. package/src/channels/msteams.ts +33 -0
  133. package/src/channels/nostr.ts +33 -0
  134. package/src/channels/qq.ts +34 -0
  135. package/src/channels/signal.ts +33 -0
  136. package/src/channels/sms.ts +34 -0
  137. package/src/channels/twitch.ts +65 -0
  138. package/src/channels/voice-call.ts +100 -0
  139. package/src/channels/whatsapp.ts +33 -0
  140. package/src/cli/chat.ts +99 -0
  141. package/src/cli/setup.ts +314 -0
  142. package/src/cli.ts +148 -6
  143. package/src/core/api-server.ts +277 -0
  144. package/src/core/audio.ts +98 -0
  145. package/src/core/context-discovery.ts +85 -0
  146. package/src/core/context-refs.ts +140 -0
  147. package/src/core/gateway.ts +106 -0
  148. package/src/core/heartbeat.ts +51 -0
  149. package/src/core/hooks.ts +105 -0
  150. package/src/core/ide-bridge.ts +133 -0
  151. package/src/core/node-network.ts +86 -0
  152. package/src/core/profiles.ts +122 -0
  153. package/src/core/sandbox.ts +100 -0
  154. package/src/core/session-manager.ts +137 -0
  155. package/src/core/vision.ts +180 -0
  156. package/src/hub/brain-seed.ts +54 -0
  157. package/src/hub/client.ts +60 -0
  158. package/src/index.ts +86 -1
  159. package/src/memory/context-compressor.ts +189 -0
  160. package/src/memory/index.ts +4 -0
  161. package/src/memory/user-profiler.ts +215 -0
  162. package/src/providers/index.ts +64 -1
  163. package/src/scheduler/cron-engine.ts +191 -0
  164. package/src/scheduler/index.ts +2 -0
  165. package/src/security/approvals.ts +143 -0
  166. package/src/security/elevated.ts +105 -0
  167. package/src/security/index.ts +6 -0
  168. package/src/security/secrets.ts +129 -0
  169. package/src/skills/builtin/index.ts +408 -0
  170. package/src/skills/marketplace.ts +113 -0
  171. package/src/skills/types.ts +42 -0
  172. package/src/studio/server.ts +1591 -791
  173. package/src/studio/templates-data.ts +178 -0
  174. package/src/studio-ui/index.html +2502 -1073
  175. package/src/tools/builtin/browser.ts +299 -0
  176. package/src/tools/builtin/home-assistant.ts +116 -0
  177. package/src/tools/builtin/index.ts +37 -28
  178. package/src/tools/builtin/rl-tools.ts +243 -0
  179. package/src/tools/builtin/vision.ts +64 -0
  180. package/src/tools/builtin/web-search.ts +126 -0
  181. package/src/tools/document-processor.ts +213 -0
  182. package/src/tools/image-generator.ts +150 -0
  183. package/src/tools/web-scraper.ts +179 -0
  184. package/src/tools/web-search.ts +180 -0
  185. package/tests/api-server.test.ts +148 -0
  186. package/tests/approvals.test.ts +89 -0
  187. package/tests/audio.test.ts +40 -0
  188. package/tests/browser.test.ts +179 -0
  189. package/tests/builtin-tools.test.ts +83 -83
  190. package/tests/channels-extra.test.ts +45 -0
  191. package/tests/context-compressor.test.ts +172 -0
  192. package/tests/context-refs.test.ts +121 -0
  193. package/tests/cron-engine.test.ts +101 -0
  194. package/tests/document-processor.test.ts +69 -0
  195. package/tests/e2e-nocode.test.ts +442 -0
  196. package/tests/elevated.test.ts +69 -0
  197. package/tests/gateway.test.ts +63 -71
  198. package/tests/home-assistant.test.ts +40 -0
  199. package/tests/hooks.test.ts +79 -0
  200. package/tests/ide-bridge.test.ts +38 -0
  201. package/tests/image-generator.test.ts +84 -0
  202. package/tests/node-network.test.ts +74 -0
  203. package/tests/profiles.test.ts +61 -0
  204. package/tests/rl-tools.test.ts +93 -0
  205. package/tests/sandbox-manager.test.ts +46 -0
  206. package/tests/secrets.test.ts +107 -0
  207. package/tests/settings-api.test.ts +148 -0
  208. package/tests/setup.test.ts +73 -0
  209. package/tests/studio.test.ts +402 -229
  210. package/tests/tools/builtin-extended.test.ts +138 -138
  211. package/tests/user-profiler.test.ts +169 -0
  212. package/tests/v090-features.test.ts +254 -0
  213. package/tests/vision.test.ts +61 -0
  214. package/tests/voice-call.test.ts +47 -0
  215. package/tests/voice-interaction.test.ts +38 -0
  216. package/tests/web-search.test.ts +155 -0
@@ -37,22 +37,56 @@ exports.StudioServer = void 0;
37
37
  const http_1 = require("http");
38
38
  const fs_1 = require("fs");
39
39
  const path_1 = require("path");
40
+ const os = __importStar(require("os"));
40
41
  const net = __importStar(require("net"));
42
+ const templates_data_1 = require("./templates-data");
43
+ const marketplace_1 = require("../skills/marketplace");
44
+ const cron_engine_1 = require("../scheduler/cron-engine");
45
+ const image_generator_1 = require("../tools/image-generator");
46
+ const document_processor_1 = require("../tools/document-processor");
41
47
  const MODULE_REGISTRY = [
42
48
  { name: 'DeepBrain', path: 'brain', port: 4001, icon: '🧠' },
43
49
  { name: 'AgentKits', path: 'kits', port: 4002, icon: '📊' },
44
50
  { name: 'Workstation', path: 'workstation', port: 4003, icon: '👤' },
45
51
  ];
52
+ // Settings config helpers
53
+ function getSettingsConfigPath() {
54
+ const dir = (0, path_1.join)(os.homedir(), '.opc');
55
+ if (!(0, fs_1.existsSync)(dir))
56
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
57
+ return (0, path_1.join)(dir, 'config.json');
58
+ }
59
+ function loadSettingsConfig() {
60
+ const p = getSettingsConfigPath();
61
+ if ((0, fs_1.existsSync)(p)) {
62
+ try {
63
+ return JSON.parse((0, fs_1.readFileSync)(p, 'utf-8'));
64
+ }
65
+ catch {
66
+ return {};
67
+ }
68
+ }
69
+ return {};
70
+ }
71
+ function saveSettingsConfig(config) {
72
+ (0, fs_1.writeFileSync)(getSettingsConfigPath(), JSON.stringify(config, null, 2));
73
+ }
46
74
  class StudioServer {
47
75
  server;
48
76
  config;
49
77
  tracer;
78
+ skillMarketplace;
79
+ cronEngine;
80
+ imageGenerator;
50
81
  constructor(config = {}) {
51
82
  this.config = {
52
83
  port: config.port || 4000,
53
84
  agentDir: config.agentDir || process.cwd(),
54
85
  staticDir: config.staticDir || (0, path_1.join)(__dirname, '../studio-ui'),
55
86
  };
87
+ this.cronEngine = new cron_engine_1.CronEngine();
88
+ this.imageGenerator = new image_generator_1.ImageGenerator();
89
+ this.skillMarketplace = new marketplace_1.SkillMarketplace();
56
90
  }
57
91
  setTracer(tracer) {
58
92
  this.tracer = tracer;
@@ -64,11 +98,19 @@ class StudioServer {
64
98
  return { ...this.config };
65
99
  }
66
100
  async start() {
101
+ const opcDir = (0, path_1.join)(os.homedir(), '.opc');
102
+ if (!(0, fs_1.existsSync)(opcDir))
103
+ (0, fs_1.mkdirSync)(opcDir, { recursive: true });
104
+ const cfgPath = (0, path_1.join)(opcDir, 'config.json');
105
+ if (!(0, fs_1.existsSync)(cfgPath))
106
+ (0, fs_1.writeFileSync)(cfgPath, JSON.stringify({}, null, 2));
67
107
  this.server = (0, http_1.createServer)((req, res) => this.handleRequest(req, res));
68
108
  this.server.listen(this.config.port);
109
+ this.cronEngine.start();
69
110
  console.log(`🎨 OPC Studio: http://localhost:${this.config.port}`);
70
111
  }
71
112
  async stop() {
113
+ this.cronEngine.stop();
72
114
  return new Promise((resolve) => {
73
115
  if (this.server) {
74
116
  this.server.close(() => resolve());
@@ -107,6 +149,195 @@ class StudioServer {
107
149
  const route = url.pathname.replace('/api/', '');
108
150
  try {
109
151
  let data;
152
+ // Dynamic agent routes
153
+ if (route === 'agents' && req.method === 'POST') {
154
+ data = await this.createAgent(req);
155
+ res.writeHead(201, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
156
+ res.end(JSON.stringify(data));
157
+ return;
158
+ }
159
+ if (route === 'agents' && req.method === 'GET') {
160
+ data = this.listAgents();
161
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
162
+ res.end(JSON.stringify(data));
163
+ return;
164
+ }
165
+ if (route === 'templates' && req.method === 'GET') {
166
+ const industry = url.searchParams.get('industry') || '';
167
+ const search = url.searchParams.get('q') || '';
168
+ data = this.getTemplates(industry, search);
169
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
170
+ res.end(JSON.stringify(data));
171
+ return;
172
+ }
173
+ if (route.match(/^templates\/[^/]+$/) && req.method === 'GET') {
174
+ const tplId = route.split('/')[1];
175
+ data = this.getTemplateById(tplId);
176
+ res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
177
+ res.end(JSON.stringify(data));
178
+ return;
179
+ }
180
+ if (route.match(/^agents\/[^/]+\/memory$/) && req.method === 'GET') {
181
+ const agentId = route.split('/')[1];
182
+ data = this.getAgentMemory(agentId);
183
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
184
+ res.end(JSON.stringify(data));
185
+ return;
186
+ }
187
+ if (route.match(/^agents\/[^/]+\/chat$/) && req.method === 'POST') {
188
+ const agentId = route.split('/')[1];
189
+ return this.handleAgentChat(req, res, agentId);
190
+ }
191
+ if (route.match(/^agents\/[^/]+$/) && req.method === 'GET') {
192
+ const agentId = route.split('/')[1];
193
+ data = this.getAgentById(agentId);
194
+ res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
195
+ res.end(JSON.stringify(data));
196
+ return;
197
+ }
198
+ if (route.match(/^agents\/[^/]+$/) && req.method === 'PUT') {
199
+ const agentId = route.split('/')[1];
200
+ data = await this.updateAgent(agentId, req);
201
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
202
+ res.end(JSON.stringify(data));
203
+ return;
204
+ }
205
+ if (route.match(/^agents\/[^/]+$/) && req.method === 'DELETE') {
206
+ const agentId = route.split('/')[1];
207
+ data = this.deleteAgent(agentId);
208
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
209
+ res.end(JSON.stringify(data));
210
+ return;
211
+ }
212
+ // --- Document upload routes ---
213
+ if (route.match(/^agents\/[^/]+\/upload$/) && req.method === 'POST') {
214
+ const agentId = route.split('/')[1];
215
+ return this.handleDocumentUpload(req, res, agentId);
216
+ }
217
+ if (route.match(/^agents\/[^/]+\/documents$/) && req.method === 'GET') {
218
+ const agentId = route.split('/')[1];
219
+ data = this.getDocumentList(agentId);
220
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
221
+ res.end(JSON.stringify(data));
222
+ return;
223
+ }
224
+ if (route.match(/^agents\/[^/]+\/documents\/[^/]+$/) && req.method === 'DELETE') {
225
+ const parts = route.split('/');
226
+ const agentId = parts[1];
227
+ const docId = parts[3];
228
+ data = this.deleteDocument(agentId, docId);
229
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
230
+ res.end(JSON.stringify(data));
231
+ return;
232
+ }
233
+ // --- Settings API routes ---
234
+ if (route === 'settings/models' && req.method === 'GET') {
235
+ const cfg = loadSettingsConfig();
236
+ data = cfg.models || { mode: 'local', provider: 'ollama', chatModel: 'qwen2.5:7b', embeddingModel: 'nomic-embed-text', providers: {} };
237
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
238
+ res.end(JSON.stringify(data));
239
+ return;
240
+ }
241
+ if (route === 'settings/models' && req.method === 'PUT') {
242
+ const body = JSON.parse(await this.readBody(req));
243
+ const cfg = loadSettingsConfig();
244
+ cfg.models = { ...(cfg.models || {}), ...body };
245
+ saveSettingsConfig(cfg);
246
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
247
+ res.end(JSON.stringify({ success: true, models: cfg.models }));
248
+ return;
249
+ }
250
+ if (route === 'settings/models/test' && req.method === 'POST') {
251
+ const body = JSON.parse(await this.readBody(req));
252
+ const { provider, apiKey, baseUrl } = body;
253
+ data = await this.testModelConnection(provider, apiKey, baseUrl);
254
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
255
+ res.end(JSON.stringify(data));
256
+ return;
257
+ }
258
+ if (route === 'settings/models/local' && req.method === 'GET') {
259
+ data = await this.detectLocalOllama();
260
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
261
+ res.end(JSON.stringify(data));
262
+ return;
263
+ }
264
+ if (route === 'settings/channels' && req.method === 'GET') {
265
+ const cfg = loadSettingsConfig();
266
+ data = cfg.channels || {};
267
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
268
+ res.end(JSON.stringify(data));
269
+ return;
270
+ }
271
+ if (route.match(/^settings\/channels\/[^/]+$/) && req.method === 'PUT') {
272
+ const channelName = route.split('/')[2];
273
+ const body = JSON.parse(await this.readBody(req));
274
+ const cfg = loadSettingsConfig();
275
+ if (!cfg.channels)
276
+ cfg.channels = {};
277
+ cfg.channels[channelName] = { ...(cfg.channels[channelName] || {}), ...body, updated: new Date().toISOString() };
278
+ saveSettingsConfig(cfg);
279
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
280
+ res.end(JSON.stringify({ success: true, channel: cfg.channels[channelName] }));
281
+ return;
282
+ }
283
+ // Web Search settings
284
+ if (route === 'settings/search' && req.method === 'GET') {
285
+ const cfg = loadSettingsConfig();
286
+ data = cfg.webSearch || { defaultEngine: 'duckduckgo', enabled: true, engines: { duckduckgo: { enabled: true } } };
287
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
288
+ res.end(JSON.stringify(data));
289
+ return;
290
+ }
291
+ if (route === 'settings/search' && req.method === 'PUT') {
292
+ const body = JSON.parse(await this.readBody(req));
293
+ const cfg = loadSettingsConfig();
294
+ cfg.webSearch = { ...(cfg.webSearch || {}), ...body, updated: new Date().toISOString() };
295
+ saveSettingsConfig(cfg);
296
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
297
+ res.end(JSON.stringify({ success: true, config: cfg.webSearch }));
298
+ return;
299
+ }
300
+ if (route === 'settings/search/test' && req.method === 'POST') {
301
+ try {
302
+ const { webSearch: doSearch } = require('../tools/web-search');
303
+ const body = JSON.parse(await this.readBody(req));
304
+ const query = body.query || 'test search';
305
+ const cfg = loadSettingsConfig();
306
+ const searchCfg = { ...(cfg.webSearch || { defaultEngine: 'duckduckgo', enabled: true, engines: { duckduckgo: { enabled: true } } }), ...body.config };
307
+ const results = await doSearch(query, searchCfg, { maxResults: 3 });
308
+ data = { success: true, results, engine: searchCfg.defaultEngine };
309
+ }
310
+ catch (e) {
311
+ data = { success: false, error: e.message };
312
+ }
313
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
314
+ res.end(JSON.stringify(data));
315
+ return;
316
+ }
317
+ if (route === 'settings/status' && req.method === 'GET') {
318
+ data = await this.getSettingsStatus();
319
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
320
+ res.end(JSON.stringify(data));
321
+ return;
322
+ }
323
+ if (route === 'settings/status/start' && req.method === 'POST') {
324
+ data = { success: true, status: 'running', message: 'Agent started' };
325
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
326
+ res.end(JSON.stringify(data));
327
+ return;
328
+ }
329
+ if (route === 'settings/status/stop' && req.method === 'POST') {
330
+ data = { success: true, status: 'stopped', message: 'Agent stopped' };
331
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
332
+ res.end(JSON.stringify(data));
333
+ return;
334
+ }
335
+ if (route === 'settings/usage' && req.method === 'GET') {
336
+ data = await this.getUsageStats();
337
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
338
+ res.end(JSON.stringify(data));
339
+ return;
340
+ }
110
341
  // Dynamic workflow routes (parameterized)
111
342
  if (route.match(/^workflows\/[^/]+\/run$/) && req.method === 'POST') {
112
343
  const wfId = route.split('/')[1];
@@ -129,6 +360,119 @@ class StudioServer {
129
360
  res.end(JSON.stringify(data));
130
361
  return;
131
362
  }
363
+ // --- Schedules API ---
364
+ if (route === 'schedules' && req.method === 'GET') {
365
+ data = this.cronEngine.listTasks();
366
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
367
+ res.end(JSON.stringify(data));
368
+ return;
369
+ }
370
+ if (route === 'schedules' && req.method === 'POST') {
371
+ const body = JSON.parse(await this.readBody(req));
372
+ data = this.cronEngine.createTask(body);
373
+ res.writeHead(201, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
374
+ res.end(JSON.stringify(data));
375
+ return;
376
+ }
377
+ if (route.match(/^schedules\/[^/]+$/) && req.method === 'PUT') {
378
+ const id = route.split('/')[1];
379
+ const body = JSON.parse(await this.readBody(req));
380
+ data = this.cronEngine.updateTask(id, body);
381
+ res.writeHead(data ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
382
+ res.end(JSON.stringify(data || { error: 'Schedule not found' }));
383
+ return;
384
+ }
385
+ if (route.match(/^schedules\/[^/]+$/) && req.method === 'DELETE') {
386
+ const id = route.split('/')[1];
387
+ const success = this.cronEngine.deleteTask(id);
388
+ res.writeHead(success ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
389
+ res.end(JSON.stringify({ success }));
390
+ return;
391
+ }
392
+ if (route.match(/^schedules\/[^/]+\/run$/) && req.method === 'POST') {
393
+ const id = route.split('/')[1];
394
+ const success = await this.cronEngine.runTask(id);
395
+ res.writeHead(success ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
396
+ res.end(JSON.stringify({ success }));
397
+ return;
398
+ }
399
+ // --- Image Generation API ---
400
+ if (route === 'image-gen/status' && req.method === 'GET') {
401
+ data = this.imageGenerator.getStatus();
402
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
403
+ res.end(JSON.stringify(data));
404
+ return;
405
+ }
406
+ if (route === 'image-gen/generate' && req.method === 'POST') {
407
+ const body = JSON.parse(await this.readBody(req));
408
+ data = await this.imageGenerator.generate(body.prompt, body);
409
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
410
+ res.end(JSON.stringify(data));
411
+ return;
412
+ }
413
+ if (route === 'image-gen/config' && req.method === 'PUT') {
414
+ const body = JSON.parse(await this.readBody(req));
415
+ const cfg = loadSettingsConfig();
416
+ cfg.imageGen = { ...(cfg.imageGen || {}), ...body };
417
+ saveSettingsConfig(cfg);
418
+ this.imageGenerator = new image_generator_1.ImageGenerator({
419
+ openaiApiKey: body.openaiApiKey,
420
+ replicateApiKey: body.replicateApiKey,
421
+ sdApiUrl: body.sdApiUrl,
422
+ });
423
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
424
+ res.end(JSON.stringify({ success: true }));
425
+ return;
426
+ }
427
+ if (route === 'first-run/status' && req.method === 'GET') {
428
+ data = await this.getFirstRunStatus();
429
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
430
+ res.end(JSON.stringify(data));
431
+ return;
432
+ }
433
+ if (route === 'first-run/complete' && req.method === 'POST') {
434
+ const body = JSON.parse(await this.readBody(req));
435
+ data = await this.completeFirstRun(body);
436
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
437
+ res.end(JSON.stringify(data));
438
+ return;
439
+ }
440
+ // === Skill Marketplace API ===
441
+ if (route === 'skills/marketplace' && req.method === 'GET') {
442
+ const category = url.searchParams.get('category') || undefined;
443
+ const search = url.searchParams.get('q') || undefined;
444
+ data = this.skillMarketplace.listAll(category, search);
445
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
446
+ res.end(JSON.stringify(data));
447
+ return;
448
+ }
449
+ if (route === 'skills/installed' && req.method === 'GET') {
450
+ data = this.skillMarketplace.getInstalled();
451
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
452
+ res.end(JSON.stringify(data));
453
+ return;
454
+ }
455
+ if (route.match(/^skills\/marketplace\/[^/]+$/) && req.method === 'GET') {
456
+ const skillId = route.split('/')[2];
457
+ data = this.skillMarketplace.getSkill(skillId);
458
+ res.writeHead(data ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
459
+ res.end(JSON.stringify(data || { error: 'Skill not found' }));
460
+ return;
461
+ }
462
+ if (route.match(/^skills\/marketplace\/[^/]+\/install$/) && req.method === 'POST') {
463
+ const skillId = route.split('/')[2];
464
+ data = this.skillMarketplace.install(skillId);
465
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
466
+ res.end(JSON.stringify(data));
467
+ return;
468
+ }
469
+ if (route.match(/^skills\/marketplace\/[^/]+\/uninstall$/) && req.method === 'DELETE') {
470
+ const skillId = route.split('/')[2];
471
+ data = this.skillMarketplace.uninstall(skillId);
472
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
473
+ res.end(JSON.stringify(data));
474
+ return;
475
+ }
132
476
  switch (route) {
133
477
  case 'modules':
134
478
  data = await this.getModulesStatus();
@@ -266,6 +610,254 @@ class StudioServer {
266
610
  res.end(JSON.stringify({ error: e.message }));
267
611
  }
268
612
  }
613
+ // --- Agent CRUD & Templates ---
614
+ getAgentsDir() {
615
+ const dir = (0, path_1.join)(os.homedir(), '.opc', 'agents');
616
+ if (!(0, fs_1.existsSync)(dir))
617
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
618
+ return dir;
619
+ }
620
+ async createAgent(req) {
621
+ const body = await this.readBody(req);
622
+ const { name, templateId, description, model, language } = JSON.parse(body);
623
+ const template = templates_data_1.TEMPLATES.find(t => t.id === templateId);
624
+ const id = `agent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
625
+ const agent = {
626
+ id,
627
+ name: name || template?.name || 'My Agent',
628
+ templateId: templateId || null,
629
+ templateName: template?.name || 'Custom',
630
+ templateIcon: template?.icon || '🤖',
631
+ description: description || template?.description || '',
632
+ model: model || template?.suggestedModel || 'gpt-4o-mini',
633
+ language: language || 'en',
634
+ systemPrompt: template?.systemPrompt || 'You are a helpful assistant.',
635
+ industry: template?.industry || 'general',
636
+ created: new Date().toISOString(),
637
+ updated: new Date().toISOString(),
638
+ messageCount: 0,
639
+ lastActive: new Date().toISOString(),
640
+ };
641
+ const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
642
+ (0, fs_1.writeFileSync)(filePath, JSON.stringify(agent, null, 2));
643
+ return agent;
644
+ }
645
+ listAgents() {
646
+ const dir = this.getAgentsDir();
647
+ const files = (0, fs_1.readdirSync)(dir).filter(f => f.endsWith('.json'));
648
+ const agents = files.map(f => {
649
+ try {
650
+ return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(dir, f), 'utf-8'));
651
+ }
652
+ catch {
653
+ return null;
654
+ }
655
+ }).filter(Boolean).sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime());
656
+ return { agents };
657
+ }
658
+ getAgentById(id) {
659
+ const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
660
+ if (!(0, fs_1.existsSync)(filePath))
661
+ return { error: 'Agent not found' };
662
+ return JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
663
+ }
664
+ async updateAgent(id, req) {
665
+ const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
666
+ if (!(0, fs_1.existsSync)(filePath))
667
+ return { error: 'Agent not found' };
668
+ const existing = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
669
+ const body = await this.readBody(req);
670
+ const updates = JSON.parse(body);
671
+ const updated = { ...existing, ...updates, id, updated: new Date().toISOString() };
672
+ (0, fs_1.writeFileSync)(filePath, JSON.stringify(updated, null, 2));
673
+ return updated;
674
+ }
675
+ deleteAgent(id) {
676
+ const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
677
+ if ((0, fs_1.existsSync)(filePath))
678
+ (0, fs_1.unlinkSync)(filePath);
679
+ return { success: true };
680
+ }
681
+ getTemplates(industry, search) {
682
+ let filtered = templates_data_1.TEMPLATES;
683
+ if (industry)
684
+ filtered = filtered.filter(t => t.industry === industry);
685
+ if (search) {
686
+ const q = search.toLowerCase();
687
+ filtered = filtered.filter(t => t.name.toLowerCase().includes(q) || t.nameZh.includes(q) ||
688
+ t.description.toLowerCase().includes(q) || t.descriptionZh.includes(q) ||
689
+ t.tags.some(tag => tag.includes(q)));
690
+ }
691
+ return { templates: filtered, industries: templates_data_1.INDUSTRIES };
692
+ }
693
+ getTemplateById(id) {
694
+ const tpl = templates_data_1.TEMPLATES.find(t => t.id === id);
695
+ return tpl || { error: 'Template not found' };
696
+ }
697
+ getAgentMemory(agentId) {
698
+ const memDir = (0, path_1.join)(this.getAgentsDir(), agentId + '-memory');
699
+ if (!(0, fs_1.existsSync)(memDir))
700
+ return { entries: [], timeline: [] };
701
+ const files = (0, fs_1.readdirSync)(memDir).filter(f => f.endsWith('.json'));
702
+ const entries = files.map(f => {
703
+ try {
704
+ return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(memDir, f), 'utf-8'));
705
+ }
706
+ catch {
707
+ return null;
708
+ }
709
+ }).filter(Boolean).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
710
+ return { entries, timeline: entries.map((e) => ({ date: e.timestamp, summary: e.summary || e.content?.slice(0, 100) })) };
711
+ }
712
+ async handleAgentChat(req, res, agentId) {
713
+ const body = JSON.parse(await this.readBody(req));
714
+ const { messages = [] } = body;
715
+ const agent = this.getAgentById(agentId);
716
+ if (agent.error) {
717
+ res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
718
+ res.end(JSON.stringify(agent));
719
+ return;
720
+ }
721
+ // Update message count
722
+ agent.messageCount = (agent.messageCount || 0) + 1;
723
+ agent.lastActive = new Date().toISOString();
724
+ agent.updated = new Date().toISOString();
725
+ const filePath = (0, path_1.join)(this.getAgentsDir(), `${agentId}.json`);
726
+ (0, fs_1.writeFileSync)(filePath, JSON.stringify(agent, null, 2));
727
+ // SSE streaming response
728
+ res.writeHead(200, {
729
+ 'Content-Type': 'text/event-stream',
730
+ 'Cache-Control': 'no-cache',
731
+ 'Connection': 'keep-alive',
732
+ 'Access-Control-Allow-Origin': '*',
733
+ });
734
+ const allMsgs = [{ role: 'system', content: agent.systemPrompt }, ...messages];
735
+ const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
736
+ // Try to call the real /v1/chat/completions endpoint
737
+ try {
738
+ const completionReq = (0, http_1.request)({
739
+ hostname: 'localhost',
740
+ port: this.config.port,
741
+ path: '/v1/chat/completions',
742
+ method: 'POST',
743
+ headers: { 'Content-Type': 'application/json' },
744
+ }, (completionRes) => {
745
+ if (completionRes.statusCode === 200) {
746
+ completionRes.pipe(res);
747
+ }
748
+ else {
749
+ // Fallback to simulated response
750
+ this.sendSimulatedResponse(res, lastMsg, agent);
751
+ }
752
+ });
753
+ completionReq.on('error', () => {
754
+ this.sendSimulatedResponse(res, lastMsg, agent);
755
+ });
756
+ completionReq.write(JSON.stringify({
757
+ model: agent.model,
758
+ messages: allMsgs,
759
+ stream: true,
760
+ }));
761
+ completionReq.end();
762
+ }
763
+ catch {
764
+ this.sendSimulatedResponse(res, lastMsg, agent);
765
+ }
766
+ }
767
+ sendSimulatedResponse(res, lastMsg, agent) {
768
+ const response = `Hello! I'm ${agent.name}. You said: "${lastMsg}"\n\nI'm ready to help you. (Note: Connect a model provider for real AI responses)`;
769
+ const words = response.split(' ');
770
+ let i = 0;
771
+ const interval = setInterval(() => {
772
+ if (i >= words.length) {
773
+ res.write('data: [DONE]\n\n');
774
+ res.end();
775
+ clearInterval(interval);
776
+ return;
777
+ }
778
+ const chunk = (i === 0 ? '' : ' ') + words[i];
779
+ res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: chunk } }] })}\n\n`);
780
+ i++;
781
+ }, 50);
782
+ }
783
+ // --- Settings Implementations ---
784
+ async detectLocalOllama() {
785
+ return new Promise((resolve) => {
786
+ const req = (0, http_1.request)({ hostname: 'localhost', port: 11434, path: '/api/tags', method: 'GET', timeout: 3000 }, (res) => {
787
+ let body = '';
788
+ res.on('data', (c) => body += c);
789
+ res.on('end', () => {
790
+ try {
791
+ const data = JSON.parse(body);
792
+ const models = (data.models || []).map((m) => ({
793
+ name: m.name, size: m.size, modified: m.modified_at,
794
+ details: m.details || {},
795
+ }));
796
+ resolve({ running: true, models });
797
+ }
798
+ catch {
799
+ resolve({ running: true, models: [] });
800
+ }
801
+ });
802
+ });
803
+ req.on('error', () => resolve({ running: false, models: [] }));
804
+ req.on('timeout', () => { req.destroy(); resolve({ running: false, models: [] }); });
805
+ req.end();
806
+ });
807
+ }
808
+ async testModelConnection(provider, apiKey, baseUrl) {
809
+ const endpoints = {
810
+ openai: { url: 'api.openai.com', path: '/v1/models' },
811
+ deepseek: { url: 'api.deepseek.com', path: '/v1/models' },
812
+ anthropic: { url: 'api.anthropic.com', path: '/v1/models' },
813
+ openrouter: { url: 'openrouter.ai', path: '/api/v1/models' },
814
+ };
815
+ const ep = endpoints[provider];
816
+ if (!ep && !baseUrl)
817
+ return { success: false, error: 'Unknown provider' };
818
+ const hostname = baseUrl ? new URL(baseUrl).hostname : ep.url;
819
+ const path = baseUrl ? '/v1/models' : ep.path;
820
+ const headers = { 'Authorization': `Bearer ${apiKey}` };
821
+ if (provider === 'anthropic') {
822
+ headers['x-api-key'] = apiKey;
823
+ headers['anthropic-version'] = '2023-06-01';
824
+ delete headers['Authorization'];
825
+ }
826
+ return new Promise((resolve) => {
827
+ const https = require('https');
828
+ const req = https.request({ hostname, path, method: 'GET', headers, timeout: 10000 }, (res) => {
829
+ resolve({ success: res.statusCode === 200, statusCode: res.statusCode });
830
+ });
831
+ req.on('error', (e) => resolve({ success: false, error: e.message }));
832
+ req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'Timeout' }); });
833
+ req.end();
834
+ });
835
+ }
836
+ async getSettingsStatus() {
837
+ const uptime = process.uptime();
838
+ const mem = process.memoryUsage();
839
+ const modules = await this.getModulesStatus();
840
+ const logPath = (0, path_1.join)(this.config.agentDir, '.opc', 'agent.log');
841
+ let recentLogs = [];
842
+ if ((0, fs_1.existsSync)(logPath)) {
843
+ const content = (0, fs_1.readFileSync)(logPath, 'utf-8');
844
+ recentLogs = content.split('\n').slice(-50);
845
+ }
846
+ return {
847
+ status: 'running',
848
+ uptime,
849
+ memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
850
+ cpu: os.loadavg(),
851
+ modules: modules.modules,
852
+ logs: recentLogs,
853
+ startedAt: new Date(Date.now() - uptime * 1000).toISOString(),
854
+ };
855
+ }
856
+ async getUsageStats() {
857
+ const cfg = loadSettingsConfig();
858
+ const usage = cfg.usage || { totalTokens: 0, totalCost: 0, daily: [], byModel: {} };
859
+ return usage;
860
+ }
269
861
  // --- API Implementations ---
270
862
  async getAgentInfo() {
271
863
  const oad = this.loadOAD();
@@ -640,6 +1232,32 @@ class StudioServer {
640
1232
  return { error: err.message };
641
1233
  }
642
1234
  }
1235
+ async getFirstRunStatus() {
1236
+ const configPath = (0, path_1.join)(os.homedir(), '.opc', 'config.json');
1237
+ const ollamaStatus = await this.detectLocalOllama();
1238
+ if (!(0, fs_1.existsSync)(configPath)) {
1239
+ return { firstRun: true, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
1240
+ }
1241
+ try {
1242
+ const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
1243
+ return { firstRun: !config.firstRunComplete, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
1244
+ }
1245
+ catch {
1246
+ return { firstRun: true, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
1247
+ }
1248
+ }
1249
+ async completeFirstRun(choices) {
1250
+ const cfg = loadSettingsConfig();
1251
+ cfg.firstRunComplete = true;
1252
+ if (choices) {
1253
+ if (choices.templateId)
1254
+ cfg.firstRunTemplate = choices.templateId;
1255
+ if (choices.model)
1256
+ cfg.models = { ...(cfg.models || {}), chatModel: choices.model };
1257
+ }
1258
+ saveSettingsConfig(cfg);
1259
+ return { success: true };
1260
+ }
643
1261
  // --- Module Proxy & Health ---
644
1262
  proxyToModule(req, res, mod, url) {
645
1263
  const targetPath = url.pathname.slice(`/${mod.path}`.length) || '/';
@@ -763,6 +1381,168 @@ class StudioServer {
763
1381
  res.write('data: [DONE]\n\n');
764
1382
  res.end();
765
1383
  }
1384
+ // --- Document upload handlers ---
1385
+ getDocumentsDir(agentId) {
1386
+ const dir = (0, path_1.join)(this.getAgentsDir(), agentId + '-documents');
1387
+ if (!(0, fs_1.existsSync)(dir))
1388
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
1389
+ return dir;
1390
+ }
1391
+ async handleDocumentUpload(req, res, agentId) {
1392
+ const corsHeaders = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' };
1393
+ try {
1394
+ // Parse multipart form data manually
1395
+ const { buffer, filename } = await this.parseMultipart(req);
1396
+ if (!filename) {
1397
+ res.writeHead(400, corsHeaders);
1398
+ res.end(JSON.stringify({ error: 'No file uploaded' }));
1399
+ return;
1400
+ }
1401
+ if (buffer.length > 50 * 1024 * 1024) {
1402
+ res.writeHead(413, corsHeaders);
1403
+ res.end(JSON.stringify({ error: 'File too large (max 50MB)' }));
1404
+ return;
1405
+ }
1406
+ // Process document
1407
+ const processor = new document_processor_1.DocumentProcessor();
1408
+ const doc = await processor.process(buffer, filename);
1409
+ // Store chunks via DeepBrain learn()
1410
+ let learnedCount = 0;
1411
+ try {
1412
+ const { Brain } = require('deepbrain');
1413
+ const oad = this.loadOAD();
1414
+ const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
1415
+ const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
1416
+ await brain.connect();
1417
+ for (const chunk of doc.chunks) {
1418
+ const content = `[Source: ${filename}] ${chunk.title}\n\n${chunk.content}`;
1419
+ if (typeof brain.store === 'function') {
1420
+ await brain.store('documents', `${doc.id}-${chunk.metadata.chunkIndex}`, content, {
1421
+ source: filename,
1422
+ docId: doc.id,
1423
+ chunkIndex: chunk.metadata.chunkIndex,
1424
+ tags: ['document-upload', filename],
1425
+ });
1426
+ }
1427
+ else if (typeof brain.learn === 'function') {
1428
+ await brain.learn(content, {
1429
+ tags: ['document-upload', filename],
1430
+ slug: `${doc.id}-${chunk.metadata.chunkIndex}`,
1431
+ });
1432
+ }
1433
+ learnedCount++;
1434
+ }
1435
+ await brain.disconnect();
1436
+ }
1437
+ catch {
1438
+ // If DeepBrain is not available, store in local memory files
1439
+ const memDir = (0, path_1.join)(this.getAgentsDir(), agentId + '-memory');
1440
+ if (!(0, fs_1.existsSync)(memDir))
1441
+ (0, fs_1.mkdirSync)(memDir, { recursive: true });
1442
+ for (const chunk of doc.chunks) {
1443
+ const entry = {
1444
+ id: `${doc.id}-${chunk.metadata.chunkIndex}`,
1445
+ content: chunk.content,
1446
+ summary: `[${filename}] ${chunk.title}`,
1447
+ timestamp: new Date().toISOString(),
1448
+ source: filename,
1449
+ docId: doc.id,
1450
+ tags: ['document-upload'],
1451
+ };
1452
+ (0, fs_1.writeFileSync)((0, path_1.join)(memDir, `${entry.id}.json`), JSON.stringify(entry, null, 2));
1453
+ learnedCount++;
1454
+ }
1455
+ }
1456
+ // Save document metadata
1457
+ const docsDir = this.getDocumentsDir(agentId);
1458
+ const docMeta = {
1459
+ id: doc.id,
1460
+ filename: doc.filename,
1461
+ format: doc.format,
1462
+ size: doc.size,
1463
+ chunks: doc.chunks.length,
1464
+ processedAt: doc.processedAt,
1465
+ };
1466
+ (0, fs_1.writeFileSync)((0, path_1.join)(docsDir, `${doc.id}.json`), JSON.stringify(docMeta, null, 2));
1467
+ res.writeHead(200, corsHeaders);
1468
+ res.end(JSON.stringify({ success: true, document: docMeta, learnedCount }));
1469
+ }
1470
+ catch (e) {
1471
+ res.writeHead(500, corsHeaders);
1472
+ res.end(JSON.stringify({ error: e.message || 'Upload failed' }));
1473
+ }
1474
+ }
1475
+ async parseMultipart(req) {
1476
+ return new Promise((resolve, reject) => {
1477
+ const contentType = req.headers['content-type'] || '';
1478
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
1479
+ if (!boundaryMatch) {
1480
+ reject(new Error('Missing multipart boundary'));
1481
+ return;
1482
+ }
1483
+ const boundary = boundaryMatch[1];
1484
+ const chunks = [];
1485
+ req.on('data', (chunk) => chunks.push(chunk));
1486
+ req.on('error', reject);
1487
+ req.on('end', () => {
1488
+ const body = Buffer.concat(chunks);
1489
+ const bodyStr = body.toString('latin1');
1490
+ const parts = bodyStr.split('--' + boundary).filter(p => p.trim() && p.trim() !== '--');
1491
+ for (const part of parts) {
1492
+ const headerEnd = part.indexOf('\r\n\r\n');
1493
+ if (headerEnd === -1)
1494
+ continue;
1495
+ const headers = part.slice(0, headerEnd);
1496
+ const filenameMatch = headers.match(/filename="([^"]+)"/);
1497
+ if (!filenameMatch)
1498
+ continue;
1499
+ const filename = filenameMatch[1];
1500
+ // Extract binary content properly
1501
+ const contentStart = body.indexOf('\r\n\r\n', body.indexOf(Buffer.from(headers.slice(0, 40), 'latin1'))) + 4;
1502
+ const nextBoundary = body.indexOf(Buffer.from('\r\n--' + boundary, 'latin1'), contentStart);
1503
+ const fileBuffer = body.slice(contentStart, nextBoundary);
1504
+ resolve({ buffer: fileBuffer, filename });
1505
+ return;
1506
+ }
1507
+ reject(new Error('No file found in upload'));
1508
+ });
1509
+ });
1510
+ }
1511
+ getDocumentList(agentId) {
1512
+ const docsDir = this.getDocumentsDir(agentId);
1513
+ const files = (0, fs_1.readdirSync)(docsDir).filter(f => f.endsWith('.json'));
1514
+ const documents = files.map(f => {
1515
+ try {
1516
+ return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(docsDir, f), 'utf-8'));
1517
+ }
1518
+ catch {
1519
+ return null;
1520
+ }
1521
+ }).filter(Boolean).sort((a, b) => new Date(b.processedAt).getTime() - new Date(a.processedAt).getTime());
1522
+ return { documents };
1523
+ }
1524
+ deleteDocument(agentId, docId) {
1525
+ const docsDir = this.getDocumentsDir(agentId);
1526
+ const docPath = (0, path_1.join)(docsDir, `${docId}.json`);
1527
+ if (!(0, fs_1.existsSync)(docPath)) {
1528
+ return { error: 'Document not found' };
1529
+ }
1530
+ // Delete document metadata
1531
+ (0, fs_1.unlinkSync)(docPath);
1532
+ // Try to delete from DeepBrain
1533
+ try {
1534
+ // Remove memory entries with this docId
1535
+ const memDir = (0, path_1.join)(this.getAgentsDir(), agentId + '-memory');
1536
+ if ((0, fs_1.existsSync)(memDir)) {
1537
+ const files = (0, fs_1.readdirSync)(memDir).filter(f => f.startsWith(docId));
1538
+ for (const f of files) {
1539
+ (0, fs_1.unlinkSync)((0, path_1.join)(memDir, f));
1540
+ }
1541
+ }
1542
+ }
1543
+ catch { /* best effort */ }
1544
+ return { success: true, deleted: docId };
1545
+ }
766
1546
  readBody(req) {
767
1547
  return new Promise((resolve, reject) => {
768
1548
  let body = '';