@yagr/agent 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.
Files changed (215) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +257 -0
  3. package/dist/agent.d.ts +21 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +47 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +293 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config/init-yagr-home.d.ts +2 -0
  12. package/dist/config/init-yagr-home.d.ts.map +1 -0
  13. package/dist/config/init-yagr-home.js +6 -0
  14. package/dist/config/init-yagr-home.js.map +1 -0
  15. package/dist/config/load-env.d.ts +2 -0
  16. package/dist/config/load-env.d.ts.map +1 -0
  17. package/dist/config/load-env.js +20 -0
  18. package/dist/config/load-env.js.map +1 -0
  19. package/dist/config/load-n8n-engine-config.d.ts +6 -0
  20. package/dist/config/load-n8n-engine-config.d.ts.map +1 -0
  21. package/dist/config/load-n8n-engine-config.js +39 -0
  22. package/dist/config/load-n8n-engine-config.js.map +1 -0
  23. package/dist/config/yagr-config-service.d.ts +51 -0
  24. package/dist/config/yagr-config-service.d.ts.map +1 -0
  25. package/dist/config/yagr-config-service.js +103 -0
  26. package/dist/config/yagr-config-service.js.map +1 -0
  27. package/dist/config/yagr-home.d.ts +4 -0
  28. package/dist/config/yagr-home.d.ts.map +1 -0
  29. package/dist/config/yagr-home.js +25 -0
  30. package/dist/config/yagr-home.js.map +1 -0
  31. package/dist/engine/engine.d.ts +15 -0
  32. package/dist/engine/engine.d.ts.map +1 -0
  33. package/dist/engine/engine.js +2 -0
  34. package/dist/engine/engine.js.map +1 -0
  35. package/dist/engine/n8n-engine.d.ts +26 -0
  36. package/dist/engine/n8n-engine.d.ts.map +1 -0
  37. package/dist/engine/n8n-engine.js +200 -0
  38. package/dist/engine/n8n-engine.js.map +1 -0
  39. package/dist/engine/yagr-engine.d.ts +17 -0
  40. package/dist/engine/yagr-engine.d.ts.map +1 -0
  41. package/dist/engine/yagr-engine.js +37 -0
  42. package/dist/engine/yagr-engine.js.map +1 -0
  43. package/dist/gateway/cli.d.ts +8 -0
  44. package/dist/gateway/cli.d.ts.map +1 -0
  45. package/dist/gateway/cli.js +50 -0
  46. package/dist/gateway/cli.js.map +1 -0
  47. package/dist/gateway/history-viewport.d.ts +14 -0
  48. package/dist/gateway/history-viewport.d.ts.map +1 -0
  49. package/dist/gateway/history-viewport.js +51 -0
  50. package/dist/gateway/history-viewport.js.map +1 -0
  51. package/dist/gateway/interactive-ui.d.ts +4 -0
  52. package/dist/gateway/interactive-ui.d.ts.map +1 -0
  53. package/dist/gateway/interactive-ui.js +430 -0
  54. package/dist/gateway/interactive-ui.js.map +1 -0
  55. package/dist/gateway/manager.d.ts +25 -0
  56. package/dist/gateway/manager.d.ts.map +1 -0
  57. package/dist/gateway/manager.js +204 -0
  58. package/dist/gateway/manager.js.map +1 -0
  59. package/dist/gateway/telegram.d.ts +30 -0
  60. package/dist/gateway/telegram.d.ts.map +1 -0
  61. package/dist/gateway/telegram.js +406 -0
  62. package/dist/gateway/telegram.js.map +1 -0
  63. package/dist/gateway/types.d.ts +19 -0
  64. package/dist/gateway/types.d.ts.map +1 -0
  65. package/dist/gateway/types.js +2 -0
  66. package/dist/gateway/types.js.map +1 -0
  67. package/dist/gateway/webui.d.ts +13 -0
  68. package/dist/gateway/webui.d.ts.map +1 -0
  69. package/dist/gateway/webui.js +663 -0
  70. package/dist/gateway/webui.js.map +1 -0
  71. package/dist/index.d.ts +23 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +15 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/llm/create-language-model.d.ts +31 -0
  76. package/dist/llm/create-language-model.d.ts.map +1 -0
  77. package/dist/llm/create-language-model.js +129 -0
  78. package/dist/llm/create-language-model.js.map +1 -0
  79. package/dist/prompt/build-system-prompt.d.ts +3 -0
  80. package/dist/prompt/build-system-prompt.d.ts.map +1 -0
  81. package/dist/prompt/build-system-prompt.js +64 -0
  82. package/dist/prompt/build-system-prompt.js.map +1 -0
  83. package/dist/runtime/completion-gate.d.ts +21 -0
  84. package/dist/runtime/completion-gate.d.ts.map +1 -0
  85. package/dist/runtime/completion-gate.js +59 -0
  86. package/dist/runtime/completion-gate.js.map +1 -0
  87. package/dist/runtime/context-compaction.d.ts +25 -0
  88. package/dist/runtime/context-compaction.d.ts.map +1 -0
  89. package/dist/runtime/context-compaction.js +197 -0
  90. package/dist/runtime/context-compaction.js.map +1 -0
  91. package/dist/runtime/outcome.d.ts +23 -0
  92. package/dist/runtime/outcome.d.ts.map +1 -0
  93. package/dist/runtime/outcome.js +89 -0
  94. package/dist/runtime/outcome.js.map +1 -0
  95. package/dist/runtime/policy-hooks.d.ts +15 -0
  96. package/dist/runtime/policy-hooks.d.ts.map +1 -0
  97. package/dist/runtime/policy-hooks.js +54 -0
  98. package/dist/runtime/policy-hooks.js.map +1 -0
  99. package/dist/runtime/required-actions.d.ts +5 -0
  100. package/dist/runtime/required-actions.d.ts.map +1 -0
  101. package/dist/runtime/required-actions.js +77 -0
  102. package/dist/runtime/required-actions.js.map +1 -0
  103. package/dist/runtime/run-engine.d.ts +15 -0
  104. package/dist/runtime/run-engine.d.ts.map +1 -0
  105. package/dist/runtime/run-engine.js +624 -0
  106. package/dist/runtime/run-engine.js.map +1 -0
  107. package/dist/setup/setup-wizard.d.ts +52 -0
  108. package/dist/setup/setup-wizard.d.ts.map +1 -0
  109. package/dist/setup/setup-wizard.js +613 -0
  110. package/dist/setup/setup-wizard.js.map +1 -0
  111. package/dist/setup/start-launcher.d.ts +22 -0
  112. package/dist/setup/start-launcher.d.ts.map +1 -0
  113. package/dist/setup/start-launcher.js +76 -0
  114. package/dist/setup/start-launcher.js.map +1 -0
  115. package/dist/setup.d.ts +20 -0
  116. package/dist/setup.d.ts.map +1 -0
  117. package/dist/setup.js +247 -0
  118. package/dist/setup.js.map +1 -0
  119. package/dist/tools/build-tools.d.ts +608 -0
  120. package/dist/tools/build-tools.d.ts.map +1 -0
  121. package/dist/tools/build-tools.js +35 -0
  122. package/dist/tools/build-tools.js.map +1 -0
  123. package/dist/tools/delete-workspace-file.d.ts +38 -0
  124. package/dist/tools/delete-workspace-file.d.ts.map +1 -0
  125. package/dist/tools/delete-workspace-file.js +31 -0
  126. package/dist/tools/delete-workspace-file.js.map +1 -0
  127. package/dist/tools/deploy.d.ts +110 -0
  128. package/dist/tools/deploy.d.ts.map +1 -0
  129. package/dist/tools/deploy.js +28 -0
  130. package/dist/tools/deploy.js.map +1 -0
  131. package/dist/tools/generate-workflow.d.ts +180 -0
  132. package/dist/tools/generate-workflow.d.ts.map +1 -0
  133. package/dist/tools/generate-workflow.js +46 -0
  134. package/dist/tools/generate-workflow.js.map +1 -0
  135. package/dist/tools/index.d.ts +20 -0
  136. package/dist/tools/index.d.ts.map +1 -0
  137. package/dist/tools/index.js +20 -0
  138. package/dist/tools/index.js.map +1 -0
  139. package/dist/tools/list-directory.d.ts +48 -0
  140. package/dist/tools/list-directory.d.ts.map +1 -0
  141. package/dist/tools/list-directory.js +61 -0
  142. package/dist/tools/list-directory.js.map +1 -0
  143. package/dist/tools/list-workflows.d.ts +18 -0
  144. package/dist/tools/list-workflows.d.ts.map +1 -0
  145. package/dist/tools/list-workflows.js +17 -0
  146. package/dist/tools/list-workflows.js.map +1 -0
  147. package/dist/tools/manage-workflow.d.ts +42 -0
  148. package/dist/tools/manage-workflow.d.ts.map +1 -0
  149. package/dist/tools/manage-workflow.js +26 -0
  150. package/dist/tools/manage-workflow.js.map +1 -0
  151. package/dist/tools/move-workspace-file.d.ts +42 -0
  152. package/dist/tools/move-workspace-file.d.ts.map +1 -0
  153. package/dist/tools/move-workspace-file.js +45 -0
  154. package/dist/tools/move-workspace-file.js.map +1 -0
  155. package/dist/tools/n8nac.d.ts +144 -0
  156. package/dist/tools/n8nac.d.ts.map +1 -0
  157. package/dist/tools/n8nac.js +349 -0
  158. package/dist/tools/n8nac.js.map +1 -0
  159. package/dist/tools/node-info.d.ts +18 -0
  160. package/dist/tools/node-info.d.ts.map +1 -0
  161. package/dist/tools/node-info.js +15 -0
  162. package/dist/tools/node-info.js.map +1 -0
  163. package/dist/tools/observer.d.ts +32 -0
  164. package/dist/tools/observer.d.ts.map +1 -0
  165. package/dist/tools/observer.js +10 -0
  166. package/dist/tools/observer.js.map +1 -0
  167. package/dist/tools/read-workspace-file.d.ts +54 -0
  168. package/dist/tools/read-workspace-file.d.ts.map +1 -0
  169. package/dist/tools/read-workspace-file.js +49 -0
  170. package/dist/tools/read-workspace-file.js.map +1 -0
  171. package/dist/tools/replace-in-workspace-file.d.ts +62 -0
  172. package/dist/tools/replace-in-workspace-file.d.ts.map +1 -0
  173. package/dist/tools/replace-in-workspace-file.js +46 -0
  174. package/dist/tools/replace-in-workspace-file.js.map +1 -0
  175. package/dist/tools/report-progress.d.ts +20 -0
  176. package/dist/tools/report-progress.d.ts.map +1 -0
  177. package/dist/tools/report-progress.js +20 -0
  178. package/dist/tools/report-progress.js.map +1 -0
  179. package/dist/tools/request-required-action.d.ts +31 -0
  180. package/dist/tools/request-required-action.d.ts.map +1 -0
  181. package/dist/tools/request-required-action.js +33 -0
  182. package/dist/tools/request-required-action.js.map +1 -0
  183. package/dist/tools/search-nodes.d.ts +18 -0
  184. package/dist/tools/search-nodes.d.ts.map +1 -0
  185. package/dist/tools/search-nodes.js +15 -0
  186. package/dist/tools/search-nodes.js.map +1 -0
  187. package/dist/tools/search-templates.d.ts +18 -0
  188. package/dist/tools/search-templates.d.ts.map +1 -0
  189. package/dist/tools/search-templates.js +15 -0
  190. package/dist/tools/search-templates.js.map +1 -0
  191. package/dist/tools/search-workspace.d.ts +55 -0
  192. package/dist/tools/search-workspace.d.ts.map +1 -0
  193. package/dist/tools/search-workspace.js +86 -0
  194. package/dist/tools/search-workspace.js.map +1 -0
  195. package/dist/tools/validate.d.ts +110 -0
  196. package/dist/tools/validate.d.ts.map +1 -0
  197. package/dist/tools/validate.js +28 -0
  198. package/dist/tools/validate.js.map +1 -0
  199. package/dist/tools/workspace-utils.d.ts +10 -0
  200. package/dist/tools/workspace-utils.d.ts.map +1 -0
  201. package/dist/tools/workspace-utils.js +63 -0
  202. package/dist/tools/workspace-utils.js.map +1 -0
  203. package/dist/tools/write-workspace-file.d.ts +46 -0
  204. package/dist/tools/write-workspace-file.d.ts.map +1 -0
  205. package/dist/tools/write-workspace-file.js +39 -0
  206. package/dist/tools/write-workspace-file.js.map +1 -0
  207. package/dist/types.d.ts +243 -0
  208. package/dist/types.d.ts.map +1 -0
  209. package/dist/types.js +2 -0
  210. package/dist/types.js.map +1 -0
  211. package/dist/webui/app.js +26759 -0
  212. package/dist/webui/app.js.map +7 -0
  213. package/dist/webui/styles.css +740 -0
  214. package/dist/webui/styles.css.map +7 -0
  215. package/package.json +72 -0
@@ -0,0 +1,663 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { execFile } from 'node:child_process';
4
+ import { readFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { promisify } from 'node:util';
8
+ import { ConfigService as N8nConfigService, N8nApiClient, WorkspaceSetupService, getDisplayProjectName, } from 'n8nac';
9
+ import { YagrAgent } from '../agent.js';
10
+ import { YagrConfigService } from '../config/yagr-config-service.js';
11
+ import { getYagrHomeDir } from '../config/yagr-home.js';
12
+ import { createOnboardingToken, getTelegramGatewayStatus, resetTelegramGateway, } from './telegram.js';
13
+ import { getYagrSetupStatus } from '../setup.js';
14
+ import { resolveLanguageModelConfig } from '../llm/create-language-model.js';
15
+ const execFileAsync = promisify(execFile);
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const DEFAULT_HOST = '127.0.0.1';
19
+ const DEFAULT_PORT = 3789;
20
+ const VALID_PROVIDERS = [
21
+ 'anthropic',
22
+ 'openai',
23
+ 'google',
24
+ 'groq',
25
+ 'mistral',
26
+ 'openrouter',
27
+ ];
28
+ const WEB_UI_HTML = `<!doctype html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="utf-8" />
32
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
33
+ <title>Yagr Web UI</title>
34
+ <link rel="stylesheet" href="/styles.css" />
35
+ <script defer src="/app.js"></script>
36
+ </head>
37
+ <body>
38
+ <div id="root"></div>
39
+ </body>
40
+ </html>`;
41
+ function isAbortError(error) {
42
+ return error instanceof Error && error.name === 'AbortError';
43
+ }
44
+ function sanitizeHost(value) {
45
+ const trimmed = value?.trim();
46
+ return trimmed || DEFAULT_HOST;
47
+ }
48
+ function sanitizePort(value) {
49
+ if (!Number.isInteger(value) || Number(value) <= 0 || Number(value) > 65535) {
50
+ return DEFAULT_PORT;
51
+ }
52
+ return Number(value);
53
+ }
54
+ function getWebUiConfig(configService = new YagrConfigService()) {
55
+ const config = configService.getLocalConfig();
56
+ return {
57
+ host: sanitizeHost(config.gateway?.webui?.host),
58
+ port: sanitizePort(config.gateway?.webui?.port),
59
+ };
60
+ }
61
+ function persistWebUiConfig(configService = new YagrConfigService(), nextConfig) {
62
+ const normalized = {
63
+ host: sanitizeHost(nextConfig.host),
64
+ port: sanitizePort(nextConfig.port),
65
+ };
66
+ configService.updateLocalConfig((localConfig) => ({
67
+ ...localConfig,
68
+ gateway: {
69
+ ...localConfig.gateway,
70
+ webui: normalized,
71
+ },
72
+ }));
73
+ return normalized;
74
+ }
75
+ export function getWebUiGatewayStatus(configService = new YagrConfigService()) {
76
+ const config = persistWebUiConfig(configService, getWebUiConfig(configService));
77
+ return {
78
+ configured: true,
79
+ host: config.host,
80
+ port: config.port,
81
+ url: `http://${config.host}:${config.port}`,
82
+ };
83
+ }
84
+ export function createWebUiGatewayRuntime(engineResolver, options = {}, configService = new YagrConfigService()) {
85
+ const status = getWebUiGatewayStatus(configService);
86
+ return {
87
+ gateway: new WebUiGateway(engineResolver, options, configService, status),
88
+ startupMessages: [
89
+ `Yagr Web UI listening at ${status.url}.`,
90
+ 'Open the local UI to configure the runtime, link Telegram, and chat with Yagr.',
91
+ ],
92
+ onboardingLink: status.url,
93
+ };
94
+ }
95
+ class WebUiGateway {
96
+ engineResolver;
97
+ options;
98
+ configService;
99
+ status;
100
+ server;
101
+ enginePromise;
102
+ agents = new Map();
103
+ constructor(engineResolver, options, configService, status) {
104
+ this.engineResolver = engineResolver;
105
+ this.options = options;
106
+ this.configService = configService;
107
+ this.status = status;
108
+ }
109
+ async start() {
110
+ if (this.server) {
111
+ return;
112
+ }
113
+ this.server = createServer(async (request, response) => {
114
+ try {
115
+ await this.handleRequest(request, response);
116
+ }
117
+ catch (error) {
118
+ this.sendJson(response, 500, {
119
+ error: error instanceof Error ? error.message : String(error),
120
+ });
121
+ }
122
+ });
123
+ await new Promise((resolve, reject) => {
124
+ this.server?.once('error', reject);
125
+ this.server?.listen(this.status.port, this.status.host, () => resolve());
126
+ });
127
+ }
128
+ async stop() {
129
+ if (!this.server) {
130
+ return;
131
+ }
132
+ const server = this.server;
133
+ this.server = undefined;
134
+ await new Promise((resolve, reject) => {
135
+ server.close((error) => {
136
+ if (error) {
137
+ reject(error);
138
+ return;
139
+ }
140
+ resolve();
141
+ });
142
+ });
143
+ }
144
+ async reply() { }
145
+ async handleRequest(request, response) {
146
+ const method = request.method ?? 'GET';
147
+ const url = new URL(request.url ?? '/', this.status.url);
148
+ if (method === 'GET' && url.pathname === '/') {
149
+ this.sendText(response, 200, WEB_UI_HTML, 'text/html; charset=utf-8');
150
+ return;
151
+ }
152
+ if (method === 'GET' && (url.pathname === '/styles.css' || url.pathname === '/app.css')) {
153
+ await this.sendStaticAsset(response, 'styles.css', 'text/css; charset=utf-8');
154
+ return;
155
+ }
156
+ if (method === 'GET' && url.pathname === '/app.js') {
157
+ await this.sendStaticAsset(response, 'app.js', 'application/javascript; charset=utf-8');
158
+ return;
159
+ }
160
+ if (method === 'GET' && url.pathname === '/api/config') {
161
+ this.sendJson(response, 200, await this.buildSnapshot());
162
+ return;
163
+ }
164
+ if (method === 'POST' && url.pathname === '/api/n8n/projects') {
165
+ const body = await this.readJson(request);
166
+ const projects = await this.fetchN8nProjects(String(body.host ?? ''), body.apiKey ? String(body.apiKey) : undefined);
167
+ const current = new N8nConfigService().getLocalConfig();
168
+ this.sendJson(response, 200, {
169
+ projects: projects.map((project) => ({ id: project.id, name: getDisplayProjectName(project) })),
170
+ selectedProjectId: current.projectId,
171
+ });
172
+ return;
173
+ }
174
+ if (method === 'POST' && url.pathname === '/api/config/n8n') {
175
+ const body = await this.readJson(request);
176
+ const warning = await this.saveN8nConfig({
177
+ host: String(body.host ?? ''),
178
+ apiKey: body.apiKey ? String(body.apiKey) : undefined,
179
+ projectId: String(body.projectId ?? ''),
180
+ syncFolder: String(body.syncFolder ?? 'workflows'),
181
+ });
182
+ this.sendJson(response, 200, {
183
+ warning,
184
+ snapshot: await this.buildSnapshot(),
185
+ });
186
+ return;
187
+ }
188
+ if (method === 'POST' && url.pathname === '/api/llm/models') {
189
+ const body = await this.readJson(request);
190
+ const provider = this.assertProvider(String(body.provider ?? ''));
191
+ const apiKey = body.apiKey ? String(body.apiKey) : this.configService.getApiKey(provider);
192
+ if (!apiKey) {
193
+ throw new Error(`No API key available for ${provider}. Save one first.`);
194
+ }
195
+ this.sendJson(response, 200, {
196
+ models: await fetchAvailableModels(provider, apiKey),
197
+ });
198
+ return;
199
+ }
200
+ if (method === 'POST' && url.pathname === '/api/config/llm') {
201
+ const body = await this.readJson(request);
202
+ const provider = this.assertProvider(String(body.provider ?? ''));
203
+ const apiKey = body.apiKey ? String(body.apiKey) : undefined;
204
+ const model = String(body.model ?? '').trim();
205
+ if (!model) {
206
+ throw new Error('Model is required.');
207
+ }
208
+ if (apiKey) {
209
+ this.configService.saveApiKey(provider, apiKey);
210
+ }
211
+ this.configService.updateLocalConfig((localConfig) => ({
212
+ ...localConfig,
213
+ provider,
214
+ model,
215
+ baseUrl: body.baseUrl ? String(body.baseUrl) : undefined,
216
+ }));
217
+ this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
218
+ return;
219
+ }
220
+ if (method === 'POST' && url.pathname === '/api/config/surfaces') {
221
+ const body = await this.readJson(request);
222
+ const enabledSurfaces = Array.isArray(body.enabledSurfaces)
223
+ ? body.enabledSurfaces.filter((surface) => surface === 'telegram' || surface === 'whatsapp')
224
+ : [];
225
+ this.configService.setEnabledGatewaySurfaces(enabledSurfaces);
226
+ this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
227
+ return;
228
+ }
229
+ if (method === 'POST' && url.pathname === '/api/telegram/configure') {
230
+ const body = await this.readJson(request);
231
+ const botToken = String(body.botToken ?? '').trim();
232
+ if (!botToken || !botToken.includes(':')) {
233
+ throw new Error('Enter a valid Telegram BotFather token.');
234
+ }
235
+ const identity = await resolveTelegramBotIdentity(botToken);
236
+ this.configService.saveTelegramBotToken(botToken);
237
+ this.configService.enableGatewaySurface('telegram');
238
+ this.configService.updateLocalConfig((localConfig) => ({
239
+ ...localConfig,
240
+ telegram: {
241
+ ...localConfig.telegram,
242
+ botUsername: identity.username,
243
+ onboardingToken: localConfig.telegram?.onboardingToken ?? createOnboardingToken(),
244
+ linkedChats: localConfig.telegram?.linkedChats ?? [],
245
+ },
246
+ }));
247
+ this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
248
+ return;
249
+ }
250
+ if (method === 'POST' && url.pathname === '/api/telegram/reset') {
251
+ resetTelegramGateway(this.configService);
252
+ this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
253
+ return;
254
+ }
255
+ if (method === 'POST' && url.pathname === '/api/chat') {
256
+ const body = await this.readJson(request);
257
+ const message = String(body.message ?? '').trim();
258
+ const sessionId = String(body.sessionId ?? randomUUID());
259
+ if (!message) {
260
+ throw new Error('Message is required.');
261
+ }
262
+ const setupStatus = getYagrSetupStatus(this.configService, new N8nConfigService());
263
+ if (!setupStatus.ready) {
264
+ throw new Error(`Yagr is not ready yet. Missing: ${setupStatus.missingSteps.join(', ')}.`);
265
+ }
266
+ const agent = await this.resolveAgent(sessionId);
267
+ const resolvedConfig = resolveLanguageModelConfig({}, this.configService);
268
+ const result = await agent.run(message, {
269
+ ...this.options,
270
+ provider: resolvedConfig.provider,
271
+ model: resolvedConfig.model,
272
+ apiKey: resolvedConfig.apiKey,
273
+ baseUrl: resolvedConfig.baseUrl,
274
+ });
275
+ this.sendJson(response, 200, {
276
+ sessionId,
277
+ response: result.text,
278
+ requiredActions: result.requiredActions,
279
+ finalState: result.finalState,
280
+ });
281
+ return;
282
+ }
283
+ if (method === 'POST' && url.pathname === '/api/chat/stream') {
284
+ const body = await this.readJson(request);
285
+ const message = String(body.message ?? '').trim();
286
+ const sessionId = String(body.sessionId ?? randomUUID());
287
+ if (!message) {
288
+ throw new Error('Message is required.');
289
+ }
290
+ await this.handleStreamingChat(response, sessionId, message);
291
+ return;
292
+ }
293
+ if (method === 'POST' && url.pathname === '/api/chat/reset') {
294
+ const body = await this.readJson(request);
295
+ const sessionId = String(body.sessionId ?? '');
296
+ if (sessionId) {
297
+ this.agents.delete(sessionId);
298
+ }
299
+ this.sendJson(response, 200, { ok: true });
300
+ return;
301
+ }
302
+ this.sendJson(response, 404, { error: 'Not found' });
303
+ }
304
+ async buildSnapshot() {
305
+ const n8nService = new N8nConfigService();
306
+ const n8nConfig = n8nService.getLocalConfig();
307
+ const setupStatus = getYagrSetupStatus(this.configService, n8nService);
308
+ const telegramStatus = getTelegramGatewayStatus(this.configService);
309
+ const webUiStatus = getWebUiGatewayStatus(this.configService);
310
+ const yagrConfig = this.configService.getLocalConfig();
311
+ const enabledSurfaces = this.configService.getEnabledGatewaySurfaces();
312
+ const startableSurfaces = enabledSurfaces.filter((surface) => surface === 'telegram' && telegramStatus.configured);
313
+ let availableModels = [];
314
+ if (yagrConfig.provider) {
315
+ const apiKey = this.configService.getApiKey(yagrConfig.provider);
316
+ if (apiKey) {
317
+ try {
318
+ availableModels = await fetchAvailableModels(yagrConfig.provider, apiKey);
319
+ }
320
+ catch {
321
+ availableModels = [];
322
+ }
323
+ }
324
+ }
325
+ return {
326
+ setupStatus,
327
+ gatewayStatus: {
328
+ enabledSurfaces,
329
+ startableSurfaces,
330
+ },
331
+ telegram: telegramStatus,
332
+ webui: webUiStatus,
333
+ yagr: {
334
+ provider: yagrConfig.provider,
335
+ model: yagrConfig.model,
336
+ baseUrl: yagrConfig.baseUrl,
337
+ providers: VALID_PROVIDERS.map((provider) => ({
338
+ provider,
339
+ apiKeyStored: this.configService.hasApiKey(provider),
340
+ })),
341
+ },
342
+ n8n: {
343
+ host: n8nConfig.host,
344
+ syncFolder: n8nConfig.syncFolder,
345
+ projectId: n8nConfig.projectId,
346
+ projectName: n8nConfig.projectName,
347
+ apiKeyStored: Boolean(n8nConfig.host && n8nService.getApiKey(n8nConfig.host)),
348
+ projects: n8nConfig.projectId && n8nConfig.projectName ? [{ id: n8nConfig.projectId, name: n8nConfig.projectName }] : [],
349
+ },
350
+ availableModels,
351
+ };
352
+ }
353
+ async fetchN8nProjects(host, apiKeyOverride) {
354
+ const normalizedHost = host.trim();
355
+ if (!normalizedHost) {
356
+ throw new Error('n8n host is required.');
357
+ }
358
+ const configService = new N8nConfigService();
359
+ const apiKey = apiKeyOverride ?? configService.getApiKey(normalizedHost);
360
+ if (!apiKey) {
361
+ throw new Error('No n8n API key available for that host.');
362
+ }
363
+ const client = new N8nApiClient({ host: normalizedHost, apiKey });
364
+ const connected = await client.testConnection();
365
+ if (!connected) {
366
+ throw new Error('Unable to connect to n8n with the provided URL and API key.');
367
+ }
368
+ return client.getProjects();
369
+ }
370
+ async saveN8nConfig(input) {
371
+ const host = input.host.trim();
372
+ const projectId = input.projectId.trim();
373
+ const syncFolder = input.syncFolder.trim() || 'workflows';
374
+ if (!host) {
375
+ throw new Error('n8n host is required.');
376
+ }
377
+ if (!projectId) {
378
+ throw new Error('Select an n8n project first.');
379
+ }
380
+ const configService = new N8nConfigService();
381
+ const apiKey = input.apiKey?.trim() || configService.getApiKey(host);
382
+ if (!apiKey) {
383
+ throw new Error('An n8n API key is required.');
384
+ }
385
+ const projects = await this.fetchN8nProjects(host, apiKey);
386
+ const selectedProject = projects.find((project) => project.id === projectId);
387
+ if (!selectedProject) {
388
+ throw new Error('The selected n8n project could not be found. Reload projects and try again.');
389
+ }
390
+ configService.saveApiKey(host, apiKey);
391
+ configService.saveBootstrapState(host, syncFolder);
392
+ const instanceIdentifier = await configService.getOrCreateInstanceIdentifier(host);
393
+ configService.saveLocalConfig({
394
+ host,
395
+ syncFolder,
396
+ projectId: selectedProject.id,
397
+ projectName: getDisplayProjectName(selectedProject),
398
+ instanceIdentifier,
399
+ });
400
+ WorkspaceSetupService.ensureWorkspaceFiles(syncFolder);
401
+ try {
402
+ await runN8nacCommand(['update-ai']);
403
+ return undefined;
404
+ }
405
+ catch (error) {
406
+ return `Workspace saved, but AGENTS.md refresh failed: ${error instanceof Error ? error.message : String(error)}`;
407
+ }
408
+ }
409
+ assertProvider(value) {
410
+ if (!VALID_PROVIDERS.includes(value)) {
411
+ throw new Error(`Unknown provider: ${value}`);
412
+ }
413
+ return value;
414
+ }
415
+ async resolveAgent(sessionId) {
416
+ const existing = this.agents.get(sessionId);
417
+ if (existing) {
418
+ return existing;
419
+ }
420
+ if (!this.enginePromise) {
421
+ this.enginePromise = this.engineResolver();
422
+ }
423
+ const engine = await this.enginePromise;
424
+ const agent = new YagrAgent(engine);
425
+ this.agents.set(sessionId, agent);
426
+ return agent;
427
+ }
428
+ async readJson(request) {
429
+ const chunks = [];
430
+ for await (const chunk of request) {
431
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
432
+ }
433
+ if (chunks.length === 0) {
434
+ return {};
435
+ }
436
+ return JSON.parse(Buffer.concat(chunks).toString('utf-8'));
437
+ }
438
+ sendJson(response, statusCode, payload) {
439
+ response.writeHead(statusCode, {
440
+ 'Content-Type': 'application/json; charset=utf-8',
441
+ 'Cache-Control': 'no-store',
442
+ });
443
+ response.end(JSON.stringify(payload));
444
+ }
445
+ sendText(response, statusCode, text, contentType) {
446
+ response.writeHead(statusCode, {
447
+ 'Content-Type': contentType,
448
+ 'Cache-Control': 'no-store',
449
+ });
450
+ response.end(text);
451
+ }
452
+ async sendStaticAsset(response, fileName, contentType) {
453
+ const assetPath = path.resolve(__dirname, '..', 'webui', fileName);
454
+ const content = await readFile(assetPath, 'utf-8');
455
+ this.sendText(response, 200, content, contentType);
456
+ }
457
+ async handleStreamingChat(response, sessionId, message) {
458
+ const setupStatus = getYagrSetupStatus(this.configService, new N8nConfigService());
459
+ if (!setupStatus.ready) {
460
+ this.sendJson(response, 400, { error: `Yagr is not ready yet. Missing: ${setupStatus.missingSteps.join(', ')}.` });
461
+ return;
462
+ }
463
+ response.writeHead(200, {
464
+ 'Content-Type': 'application/x-ndjson; charset=utf-8',
465
+ 'Cache-Control': 'no-store',
466
+ Connection: 'keep-alive',
467
+ 'X-Accel-Buffering': 'no',
468
+ });
469
+ const abortController = new AbortController();
470
+ let runFinished = false;
471
+ let streamedText = '';
472
+ const handleConnectionClose = () => {
473
+ if (!runFinished && !abortController.signal.aborted) {
474
+ abortController.abort();
475
+ }
476
+ };
477
+ response.on('close', handleConnectionClose);
478
+ const writeEvent = (event) => {
479
+ if (response.writableEnded || response.destroyed) {
480
+ return;
481
+ }
482
+ response.write(`${JSON.stringify(event)}\n`);
483
+ };
484
+ const pushPhaseEvent = (event) => {
485
+ if (event.status !== 'started') {
486
+ return;
487
+ }
488
+ writeEvent({
489
+ type: 'phase',
490
+ phase: event.phase,
491
+ status: event.status,
492
+ message: event.message,
493
+ });
494
+ };
495
+ const pushStateEvent = (event) => {
496
+ if (event.state === 'running' || event.state === 'streaming' || event.state === 'completed') {
497
+ return;
498
+ }
499
+ writeEvent({
500
+ type: 'state',
501
+ state: event.state,
502
+ message: event.message,
503
+ });
504
+ };
505
+ const pushToolEvent = (event) => {
506
+ if (event.type === 'status') {
507
+ writeEvent({
508
+ type: 'progress',
509
+ tone: 'info',
510
+ title: event.toolName === 'reportProgress' ? 'Progress' : `Tool ${event.toolName}`,
511
+ detail: event.message,
512
+ });
513
+ return;
514
+ }
515
+ if (event.type === 'command-end') {
516
+ if (event.exitCode === 0) {
517
+ return;
518
+ }
519
+ writeEvent({
520
+ type: 'progress',
521
+ tone: 'error',
522
+ title: `${event.toolName} failed`,
523
+ detail: event.message,
524
+ });
525
+ return;
526
+ }
527
+ };
528
+ const pushCompactionEvent = (event) => {
529
+ writeEvent({
530
+ type: 'progress',
531
+ tone: 'info',
532
+ title: 'Context compacted',
533
+ detail: `${event.messagesCompacted} messages folded to keep the run responsive.`,
534
+ });
535
+ };
536
+ try {
537
+ writeEvent({
538
+ type: 'start',
539
+ sessionId,
540
+ message: 'Run started. Inspecting workspace and request.',
541
+ });
542
+ const agent = await this.resolveAgent(sessionId);
543
+ const resolvedConfig = resolveLanguageModelConfig({}, this.configService);
544
+ const result = await agent.run(message, {
545
+ ...this.options,
546
+ provider: resolvedConfig.provider,
547
+ model: resolvedConfig.model,
548
+ apiKey: resolvedConfig.apiKey,
549
+ baseUrl: resolvedConfig.baseUrl,
550
+ onPhaseChange: async (event) => {
551
+ pushPhaseEvent(event);
552
+ await this.options.onPhaseChange?.(event);
553
+ },
554
+ onStateChange: async (event) => {
555
+ pushStateEvent(event);
556
+ await this.options.onStateChange?.(event);
557
+ },
558
+ onToolEvent: async (event) => {
559
+ pushToolEvent(event);
560
+ await this.options.onToolEvent?.(event);
561
+ },
562
+ onCompaction: async (event) => {
563
+ pushCompactionEvent(event);
564
+ await this.options.onCompaction?.(event);
565
+ },
566
+ onTextDelta: async (delta) => {
567
+ streamedText += delta;
568
+ writeEvent({ type: 'text-delta', delta });
569
+ await this.options.onTextDelta?.(delta);
570
+ },
571
+ abortSignal: abortController.signal,
572
+ });
573
+ runFinished = true;
574
+ writeEvent({
575
+ type: 'final',
576
+ sessionId,
577
+ response: result.text,
578
+ finalState: result.finalState,
579
+ requiredActions: result.requiredActions.map((action) => ({
580
+ title: action.title,
581
+ message: action.message,
582
+ })),
583
+ });
584
+ }
585
+ catch (error) {
586
+ if (isAbortError(error)) {
587
+ runFinished = true;
588
+ writeEvent({
589
+ type: 'final',
590
+ sessionId,
591
+ response: streamedText,
592
+ finalState: 'stopped',
593
+ requiredActions: [],
594
+ });
595
+ }
596
+ else {
597
+ writeEvent({
598
+ type: 'error',
599
+ error: error instanceof Error ? error.message : String(error),
600
+ });
601
+ }
602
+ }
603
+ finally {
604
+ response.off('close', handleConnectionClose);
605
+ response.end();
606
+ }
607
+ }
608
+ }
609
+ async function fetchAvailableModels(provider, apiKey) {
610
+ const endpoints = {
611
+ openrouter: {
612
+ url: 'https://openrouter.ai/api/v1/models',
613
+ map: (data) => data.data?.map((model) => model.id) ?? [],
614
+ },
615
+ openai: {
616
+ url: 'https://api.openai.com/v1/models',
617
+ map: (data) => data.data?.map((model) => model.id) ?? [],
618
+ },
619
+ groq: {
620
+ url: 'https://api.groq.com/openai/v1/models',
621
+ map: (data) => data.data?.map((model) => model.id) ?? [],
622
+ },
623
+ mistral: {
624
+ url: 'https://api.mistral.ai/v1/models',
625
+ map: (data) => data.data?.map((model) => model.id) ?? [],
626
+ },
627
+ };
628
+ const endpoint = endpoints[provider];
629
+ if (!endpoint) {
630
+ return [];
631
+ }
632
+ const response = await fetch(endpoint.url, {
633
+ headers: {
634
+ Authorization: `Bearer ${apiKey}`,
635
+ 'Content-Type': 'application/json',
636
+ },
637
+ });
638
+ if (!response.ok) {
639
+ throw new Error(`${response.status} ${response.statusText}`);
640
+ }
641
+ const payload = await response.json();
642
+ return endpoint.map(payload).sort((left, right) => left.localeCompare(right));
643
+ }
644
+ async function resolveTelegramBotIdentity(botToken) {
645
+ const { Telegraf } = await import('telegraf');
646
+ const bot = new Telegraf(botToken);
647
+ const me = await bot.telegram.getMe();
648
+ if (!me.username) {
649
+ throw new Error('Telegram bot username is missing. Configure the bot with BotFather first.');
650
+ }
651
+ return {
652
+ username: me.username,
653
+ firstName: me.first_name,
654
+ };
655
+ }
656
+ async function runN8nacCommand(args) {
657
+ const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
658
+ await execFileAsync(command, ['--yes', 'n8nac', ...args], {
659
+ cwd: getYagrHomeDir(),
660
+ env: process.env,
661
+ });
662
+ }
663
+ //# sourceMappingURL=webui.js.map