kindflow 0.2.0 → 0.4.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/daemon.js CHANGED
@@ -1,11 +1,3018 @@
1
1
  import {
2
- startDaemon
3
- } from "./chunk-UGIMNC7O.js";
4
- import "./chunk-3AMW7NXZ.js";
5
- import "./chunk-2YT53FBK.js";
6
- import "./chunk-2UX7Z5TH.js";
7
- import "./chunk-GZFR7ELC.js";
8
- import "./chunk-P5P73L35.js";
2
+ createAgentFromTemplate,
3
+ deleteAgentDir,
4
+ generateSlug,
5
+ listAgentCommands,
6
+ readAgent,
7
+ scanAgents,
8
+ writeAgentConfig
9
+ } from "./chunk-IIHUCTI3.js";
10
+ import {
11
+ detectMode,
12
+ resolvePaths
13
+ } from "./chunk-S6UDV4I6.js";
14
+ import {
15
+ ImapClient,
16
+ buildConsentUrl,
17
+ buildXOAuth2Token,
18
+ exchangeCodeForTokens,
19
+ refreshAccessToken
20
+ } from "./chunk-3AMW7NXZ.js";
21
+ import {
22
+ createTelegramClient
23
+ } from "./chunk-2YT53FBK.js";
24
+ import {
25
+ createIpcHub,
26
+ createMessage
27
+ } from "./chunk-2UX7Z5TH.js";
28
+ import {
29
+ createS3Bucket
30
+ } from "./chunk-GZFR7ELC.js";
31
+ import {
32
+ createDatabaseClient
33
+ } from "./chunk-P5P73L35.js";
34
+
35
+ // ../daemon/src/config/config-store.ts
36
+ import { join } from "path";
37
+ import { homedir } from "os";
38
+
39
+ // ../store/src/store.ts
40
+ import initSqlJs from "sql.js";
41
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
42
+ import { dirname } from "path";
43
+
44
+ // ../store/src/table.ts
45
+ var buildWhere = (where) => {
46
+ const keys = Object.keys(where);
47
+ if (keys.length === 0) return { clause: "", values: [] };
48
+ const conditions = keys.map((k) => `${k} = ?`);
49
+ const values = keys.map((k) => where[k]);
50
+ return { clause: `WHERE ${conditions.join(" AND ")}`, values };
51
+ };
52
+ var rowsToObjects = (columns, values) => values.map((row) => {
53
+ const obj = {};
54
+ columns.forEach((col, i) => {
55
+ obj[col] = row[i];
56
+ });
57
+ return obj;
58
+ });
59
+ var buildColumnSql = (col, def) => {
60
+ let sql = `${col} ${def.type.toUpperCase()}`;
61
+ if (def.primaryKey) sql += " PRIMARY KEY";
62
+ if (!def.nullable && !def.primaryKey) sql += " NOT NULL";
63
+ if (def.default !== void 0) {
64
+ sql += ` DEFAULT ${typeof def.default === "string" ? `'${def.default}'` : def.default}`;
65
+ }
66
+ if (def.check) {
67
+ sql += ` CHECK(${col} IN (${def.check.map((v) => `'${v}'`).join(", ")}))`;
68
+ }
69
+ return sql;
70
+ };
71
+ var createTable = (db, saveFn, name, schema) => {
72
+ const columns = Object.entries(schema).map(([col, def]) => buildColumnSql(col, def));
73
+ db.run(`CREATE TABLE IF NOT EXISTS ${name} (${columns.join(", ")})`);
74
+ return {
75
+ /** Insert a new row into the table. */
76
+ insert(row) {
77
+ const keys = Object.keys(row);
78
+ const placeholders = keys.map(() => "?").join(", ");
79
+ const values = keys.map((k) => row[k]);
80
+ db.run(`INSERT INTO ${name} (${keys.join(", ")}) VALUES (${placeholders})`, values);
81
+ saveFn();
82
+ },
83
+ /** Find rows matching an optional WHERE clause. Returns all rows if no clause given. */
84
+ findMany(clause) {
85
+ let result;
86
+ if (clause) {
87
+ const { clause: where, values } = buildWhere(clause.where);
88
+ result = db.exec(`SELECT * FROM ${name} ${where}`, values);
89
+ } else {
90
+ result = db.exec(`SELECT * FROM ${name}`);
91
+ }
92
+ if (result.length === 0) return [];
93
+ return rowsToObjects(result[0].columns, result[0].values);
94
+ },
95
+ /** Find a single row matching the WHERE clause. Returns undefined if not found. */
96
+ findOne(clause) {
97
+ const { clause: where, values } = buildWhere(clause.where);
98
+ const result = db.exec(`SELECT * FROM ${name} ${where} LIMIT 1`, values);
99
+ if (result.length === 0 || result[0].values.length === 0) return void 0;
100
+ return rowsToObjects(result[0].columns, result[0].values)[0];
101
+ },
102
+ /** Update rows matching the WHERE clause. Returns the number of modified rows. */
103
+ update(values, clause) {
104
+ const setKeys = Object.keys(values);
105
+ const setClause = setKeys.map((k) => `${k} = ?`).join(", ");
106
+ const setValues = setKeys.map((k) => values[k]);
107
+ const { clause: where, values: whereValues } = buildWhere(clause.where);
108
+ db.run(`UPDATE ${name} SET ${setClause} ${where}`, [...setValues, ...whereValues]);
109
+ const modified = db.getRowsModified();
110
+ saveFn();
111
+ return modified;
112
+ },
113
+ /** Delete rows matching the WHERE clause. Returns the number of deleted rows. */
114
+ deleteRows(clause) {
115
+ const { clause: where, values } = buildWhere(clause.where);
116
+ db.run(`DELETE FROM ${name} ${where}`, values);
117
+ const modified = db.getRowsModified();
118
+ saveFn();
119
+ return modified;
120
+ },
121
+ /** Return every row in the table. */
122
+ all() {
123
+ const result = db.exec(`SELECT * FROM ${name}`);
124
+ if (result.length === 0) return [];
125
+ return rowsToObjects(result[0].columns, result[0].values);
126
+ }
127
+ };
128
+ };
129
+
130
+ // ../store/src/store.ts
131
+ var createStore = async (options) => {
132
+ const SQL = await initSqlJs();
133
+ let db;
134
+ try {
135
+ const buffer = readFileSync(options.path);
136
+ db = new SQL.Database(buffer);
137
+ } catch {
138
+ mkdirSync(dirname(options.path), { recursive: true });
139
+ db = new SQL.Database();
140
+ }
141
+ const save = () => {
142
+ const data = db.export();
143
+ writeFileSync(options.path, Buffer.from(data));
144
+ };
145
+ const tables = /* @__PURE__ */ new Map();
146
+ for (const [name, schema] of Object.entries(options.tables)) {
147
+ tables.set(name, createTable(db, save, name, schema));
148
+ }
149
+ save();
150
+ return {
151
+ table(name) {
152
+ const t = tables.get(name);
153
+ if (!t) throw new Error(`Unknown table: ${name}`);
154
+ return t;
155
+ },
156
+ query(sql, params) {
157
+ const result = params ? db.exec(sql, params) : db.exec(sql);
158
+ if (result.length === 0) return [];
159
+ const { columns, values } = result[0];
160
+ return values.map((row) => {
161
+ const obj = {};
162
+ columns.forEach((col, i) => {
163
+ obj[col] = row[i];
164
+ });
165
+ return obj;
166
+ });
167
+ },
168
+ close() {
169
+ save();
170
+ db.close();
171
+ }
172
+ };
173
+ };
174
+
175
+ // ../daemon/src/config/schema.ts
176
+ var configSchema = {
177
+ /**
178
+ * Singleton helper process configurations.
179
+ * The `kind` column is unique — only one helper per kind.
180
+ */
181
+ helpers: {
182
+ id: { type: "text", primaryKey: true },
183
+ kind: { type: "text" },
184
+ name: { type: "text" },
185
+ config: { type: "text" },
186
+ auto_start: { type: "integer", default: 0 },
187
+ created_at: { type: "text" },
188
+ updated_at: { type: "text" }
189
+ },
190
+ /**
191
+ * Global S3 bucket profiles. Shared across all agents.
192
+ * Credentials are referenced by AWS profile name (stored in ~/.aws/credentials).
193
+ */
194
+ s3_profiles: {
195
+ id: { type: "text", primaryKey: true },
196
+ name: { type: "text" },
197
+ bucket: { type: "text" },
198
+ region: { type: "text" },
199
+ aws_profile: { type: "text", nullable: true },
200
+ verified: { type: "integer", default: 0 },
201
+ created_at: { type: "text" }
202
+ },
203
+ /**
204
+ * Global key-value settings for the daemon (ports, etc.).
205
+ */
206
+ settings: {
207
+ key: { type: "text", primaryKey: true },
208
+ value: { type: "text" }
209
+ },
210
+ /**
211
+ * Gmail accounts connected via OAuth.
212
+ * Refresh tokens are AES-256-CBC encrypted.
213
+ */
214
+ email_accounts: {
215
+ id: { type: "text", primaryKey: true },
216
+ name: { type: "text" },
217
+ email: { type: "text" },
218
+ refresh_token: { type: "text" },
219
+ created_at: { type: "text" },
220
+ updated_at: { type: "text" }
221
+ },
222
+ /**
223
+ * Routing rules: email account -> target agent.
224
+ * When a new email arrives on the account, it is forwarded
225
+ * to the target agent via IPC prompt.
226
+ */
227
+ email_connections: {
228
+ id: { type: "text", primaryKey: true },
229
+ account_id: { type: "text" },
230
+ target_agent: { type: "text" },
231
+ command: { type: "text" },
232
+ enabled: { type: "integer", default: 1 },
233
+ created_at: { type: "text" },
234
+ updated_at: { type: "text" }
235
+ },
236
+ /**
237
+ * Global database connection profiles. Shared across all agents.
238
+ * Passwords are AES-256-CBC encrypted.
239
+ */
240
+ database_profiles: {
241
+ id: { type: "text", primaryKey: true },
242
+ name: { type: "text" },
243
+ db_type: { type: "text" },
244
+ host: { type: "text", nullable: true },
245
+ port: { type: "integer", nullable: true },
246
+ database: { type: "text" },
247
+ username: { type: "text", nullable: true },
248
+ password: { type: "text", nullable: true },
249
+ ssl_mode: { type: "text", nullable: true },
250
+ verified: { type: "integer", default: 0 },
251
+ created_at: { type: "text" }
252
+ },
253
+ /**
254
+ * OAuth client credentials (one row per provider, e.g. "gmail").
255
+ * Client secret is AES-256-CBC encrypted.
256
+ */
257
+ oauth_credentials: {
258
+ key: { type: "text", primaryKey: true },
259
+ client_id: { type: "text" },
260
+ client_secret: { type: "text" }
261
+ },
262
+ /**
263
+ * Telegram bot profiles. Bot tokens are AES-256-CBC encrypted.
264
+ * Each bot can be connected to multiple agents via telegram_connections.
265
+ */
266
+ telegram_bots: {
267
+ id: { type: "text", primaryKey: true },
268
+ name: { type: "text" },
269
+ token: { type: "text" },
270
+ bot_username: { type: "text" },
271
+ polling: { type: "integer", default: 1 },
272
+ webhook_url: { type: "text", nullable: true },
273
+ verified: { type: "integer", default: 0 },
274
+ created_at: { type: "text" }
275
+ },
276
+ /**
277
+ * Routing rules: Telegram bot + chat -> target agent.
278
+ * When a message arrives on the bot matching the chat_id,
279
+ * it is forwarded to the target agent via IPC prompt.
280
+ */
281
+ telegram_connections: {
282
+ id: { type: "text", primaryKey: true },
283
+ bot_id: { type: "text" },
284
+ target_agent: { type: "text" },
285
+ chat_id: { type: "text" },
286
+ enabled: { type: "integer", default: 1 },
287
+ created_at: { type: "text" }
288
+ },
289
+ /**
290
+ * Global HTTP endpoints. Each endpoint exposes a unique URL path
291
+ * for external systems to trigger agents via POST /api/trigger/:path.
292
+ */
293
+ http_endpoints: {
294
+ id: { type: "text", primaryKey: true },
295
+ name: { type: "text" },
296
+ path: { type: "text" },
297
+ api_key: { type: "text", nullable: true },
298
+ enabled: { type: "integer", default: 1 },
299
+ created_at: { type: "text" }
300
+ },
301
+ /**
302
+ * Routing rules: HTTP endpoint -> target agent.
303
+ * When a POST hits the endpoint, the request is forwarded
304
+ * to the target agent via IPC prompt.
305
+ */
306
+ http_connections: {
307
+ id: { type: "text", primaryKey: true },
308
+ endpoint_id: { type: "text" },
309
+ target_agent: { type: "text" },
310
+ command: { type: "text", nullable: true },
311
+ enabled: { type: "integer", default: 1 },
312
+ created_at: { type: "text" }
313
+ },
314
+ /**
315
+ * Global cron jobs. Each job fires on a schedule and routes
316
+ * to one or more agents via cron_connections.
317
+ */
318
+ cron_jobs: {
319
+ id: { type: "text", primaryKey: true },
320
+ name: { type: "text" },
321
+ schedule: { type: "text" },
322
+ enabled: { type: "integer", default: 1 },
323
+ created_at: { type: "text" }
324
+ },
325
+ /**
326
+ * Routing rules: cron job -> target agent.
327
+ * When a cron job fires, each enabled connection triggers
328
+ * the target agent with the configured command (or the job
329
+ * name as a free-form prompt if command is null).
330
+ */
331
+ cron_connections: {
332
+ id: { type: "text", primaryKey: true },
333
+ cron_job_id: { type: "text" },
334
+ target_agent: { type: "text" },
335
+ command: { type: "text", nullable: true },
336
+ enabled: { type: "integer", default: 1 },
337
+ created_at: { type: "text" }
338
+ },
339
+ /**
340
+ * Routing rules: S3 profile -> target agent.
341
+ * Passive capability — no command, just a purpose description.
342
+ * Auto-generates a capability file in the agent directory.
343
+ */
344
+ s3_connections: {
345
+ id: { type: "text", primaryKey: true },
346
+ profile_id: { type: "text" },
347
+ target_agent: { type: "text" },
348
+ purpose: { type: "text" },
349
+ enabled: { type: "integer", default: 1 },
350
+ created_at: { type: "text" }
351
+ },
352
+ /**
353
+ * Routing rules: database profile -> target agent.
354
+ * Passive capability — no command, just a purpose description.
355
+ * Auto-generates a capability file in the agent directory.
356
+ */
357
+ database_connections: {
358
+ id: { type: "text", primaryKey: true },
359
+ profile_id: { type: "text" },
360
+ target_agent: { type: "text" },
361
+ purpose: { type: "text" },
362
+ enabled: { type: "integer", default: 1 },
363
+ created_at: { type: "text" }
364
+ }
365
+ };
366
+
367
+ // ../daemon/src/config/config-store.ts
368
+ var getDataDir = () => process.env["KINDFLOW_DATA_DIR"] ?? join(homedir(), ".kindflow");
369
+ var helperToRow = (helper) => ({
370
+ id: helper.id,
371
+ kind: helper.kind,
372
+ name: helper.name,
373
+ config: JSON.stringify(helper.config),
374
+ auto_start: helper.autoStart ? 1 : 0,
375
+ created_at: helper.createdAt,
376
+ updated_at: helper.updatedAt
377
+ });
378
+ var rowToHelper = (row) => ({
379
+ id: row["id"],
380
+ kind: row["kind"],
381
+ name: row["name"],
382
+ config: JSON.parse(row["config"]),
383
+ autoStart: row["auto_start"] === 1,
384
+ createdAt: row["created_at"],
385
+ updatedAt: row["updated_at"]
386
+ });
387
+ var emailAccountToRow = (account) => ({
388
+ id: account.id,
389
+ name: account.name,
390
+ email: account.email,
391
+ refresh_token: account.refreshToken,
392
+ created_at: account.createdAt,
393
+ updated_at: account.updatedAt
394
+ });
395
+ var rowToEmailAccount = (row) => ({
396
+ id: row["id"],
397
+ name: row["name"],
398
+ email: row["email"],
399
+ refreshToken: row["refresh_token"],
400
+ createdAt: row["created_at"],
401
+ updatedAt: row["updated_at"]
402
+ });
403
+ var emailConnectionToRow = (conn) => ({
404
+ id: conn.id,
405
+ account_id: conn.accountId,
406
+ target_agent: conn.targetAgent,
407
+ command: conn.command,
408
+ enabled: conn.enabled ? 1 : 0,
409
+ created_at: conn.createdAt,
410
+ updated_at: conn.updatedAt
411
+ });
412
+ var rowToEmailConnection = (row) => ({
413
+ id: row["id"],
414
+ accountId: row["account_id"],
415
+ targetAgent: row["target_agent"],
416
+ command: row["command"],
417
+ enabled: row["enabled"] === 1,
418
+ createdAt: row["created_at"],
419
+ updatedAt: row["updated_at"]
420
+ });
421
+ var rowToTelegramBot = (row) => ({
422
+ id: row["id"],
423
+ name: row["name"],
424
+ token: row["token"],
425
+ botUsername: row["bot_username"],
426
+ polling: row["polling"] === 1,
427
+ webhookUrl: row["webhook_url"] || null,
428
+ verified: row["verified"] === 1,
429
+ createdAt: row["created_at"]
430
+ });
431
+ var telegramBotToRow = (bot) => ({
432
+ id: bot.id,
433
+ name: bot.name,
434
+ token: bot.token,
435
+ bot_username: bot.botUsername,
436
+ polling: bot.polling ? 1 : 0,
437
+ webhook_url: bot.webhookUrl ?? "",
438
+ verified: bot.verified ? 1 : 0,
439
+ created_at: bot.createdAt
440
+ });
441
+ var rowToTelegramConnection = (row) => ({
442
+ id: row["id"],
443
+ botId: row["bot_id"],
444
+ targetAgent: row["target_agent"],
445
+ chatId: row["chat_id"],
446
+ enabled: row["enabled"] === 1,
447
+ createdAt: row["created_at"]
448
+ });
449
+ var telegramConnectionToRow = (conn) => ({
450
+ id: conn.id,
451
+ bot_id: conn.botId,
452
+ target_agent: conn.targetAgent,
453
+ chat_id: conn.chatId,
454
+ enabled: conn.enabled ? 1 : 0,
455
+ created_at: conn.createdAt
456
+ });
457
+ var rowToHttpEndpoint = (row) => ({
458
+ id: row["id"],
459
+ name: row["name"],
460
+ path: row["path"],
461
+ apiKey: row["api_key"] || null,
462
+ enabled: row["enabled"] === 1,
463
+ createdAt: row["created_at"]
464
+ });
465
+ var httpEndpointToRow = (endpoint) => ({
466
+ id: endpoint.id,
467
+ name: endpoint.name,
468
+ path: endpoint.path,
469
+ api_key: endpoint.apiKey ?? "",
470
+ enabled: endpoint.enabled ? 1 : 0,
471
+ created_at: endpoint.createdAt
472
+ });
473
+ var rowToHttpConnection = (row) => ({
474
+ id: row["id"],
475
+ endpointId: row["endpoint_id"],
476
+ targetAgent: row["target_agent"],
477
+ command: row["command"] || null,
478
+ enabled: row["enabled"] === 1,
479
+ createdAt: row["created_at"]
480
+ });
481
+ var httpConnectionToRow = (conn) => ({
482
+ id: conn.id,
483
+ endpoint_id: conn.endpointId,
484
+ target_agent: conn.targetAgent,
485
+ command: conn.command ?? "",
486
+ enabled: conn.enabled ? 1 : 0,
487
+ created_at: conn.createdAt
488
+ });
489
+ var rowToS3Connection = (row) => ({
490
+ id: row["id"],
491
+ profileId: row["profile_id"],
492
+ targetAgent: row["target_agent"],
493
+ purpose: row["purpose"],
494
+ enabled: row["enabled"] === 1,
495
+ createdAt: row["created_at"]
496
+ });
497
+ var s3ConnectionToRow = (conn) => ({
498
+ id: conn.id,
499
+ profile_id: conn.profileId,
500
+ target_agent: conn.targetAgent,
501
+ purpose: conn.purpose,
502
+ enabled: conn.enabled ? 1 : 0,
503
+ created_at: conn.createdAt
504
+ });
505
+ var rowToDatabaseConnection = (row) => ({
506
+ id: row["id"],
507
+ profileId: row["profile_id"],
508
+ targetAgent: row["target_agent"],
509
+ purpose: row["purpose"],
510
+ enabled: row["enabled"] === 1,
511
+ createdAt: row["created_at"]
512
+ });
513
+ var databaseConnectionToRow = (conn) => ({
514
+ id: conn.id,
515
+ profile_id: conn.profileId,
516
+ target_agent: conn.targetAgent,
517
+ purpose: conn.purpose,
518
+ enabled: conn.enabled ? 1 : 0,
519
+ created_at: conn.createdAt
520
+ });
521
+ var rowToCronJob = (row) => ({
522
+ id: row["id"],
523
+ name: row["name"],
524
+ schedule: row["schedule"],
525
+ enabled: row["enabled"] === 1,
526
+ createdAt: row["created_at"]
527
+ });
528
+ var cronJobToRow = (job) => ({
529
+ id: job.id,
530
+ name: job.name,
531
+ schedule: job.schedule,
532
+ enabled: job.enabled ? 1 : 0,
533
+ created_at: job.createdAt
534
+ });
535
+ var rowToCronConnection = (row) => ({
536
+ id: row["id"],
537
+ cronJobId: row["cron_job_id"],
538
+ targetAgent: row["target_agent"],
539
+ command: row["command"],
540
+ enabled: row["enabled"] === 1,
541
+ createdAt: row["created_at"]
542
+ });
543
+ var cronConnectionToRow = (conn) => ({
544
+ id: conn.id,
545
+ cron_job_id: conn.cronJobId,
546
+ target_agent: conn.targetAgent,
547
+ command: conn.command ?? "",
548
+ enabled: conn.enabled ? 1 : 0,
549
+ created_at: conn.createdAt
550
+ });
551
+ var createConfigStore = async (dataDir) => {
552
+ const dir = dataDir ?? getDataDir();
553
+ const dbPath = join(dir, "config.db");
554
+ const store = await createStore({
555
+ path: dbPath,
556
+ tables: configSchema
557
+ });
558
+ const helpers = store.table("helpers");
559
+ const s3Profiles = store.table("s3_profiles");
560
+ const dbProfiles = store.table("database_profiles");
561
+ const settings = store.table("settings");
562
+ const emailAccounts = store.table("email_accounts");
563
+ const emailConnections = store.table("email_connections");
564
+ const oauthCredentials = store.table("oauth_credentials");
565
+ const telegramBots = store.table("telegram_bots");
566
+ const telegramConnections = store.table("telegram_connections");
567
+ const httpEndpoints = store.table("http_endpoints");
568
+ const httpConnectionsTable = store.table("http_connections");
569
+ const cronJobsTable = store.table("cron_jobs");
570
+ const cronConnectionsTable = store.table("cron_connections");
571
+ const s3ConnectionsTable = store.table("s3_connections");
572
+ const databaseConnectionsTable = store.table("database_connections");
573
+ const rowToDb = (row) => ({
574
+ id: row["id"],
575
+ name: row["name"],
576
+ type: row["db_type"],
577
+ host: row["host"] || null,
578
+ port: row["port"] || null,
579
+ database: row["database"],
580
+ username: row["username"] || null,
581
+ password: row["password"] || null,
582
+ sslMode: row["ssl_mode"] || null,
583
+ verified: row["verified"] === 1,
584
+ createdAt: row["created_at"]
585
+ });
586
+ const rowToS3 = (row) => ({
587
+ id: row["id"],
588
+ name: row["name"],
589
+ bucket: row["bucket"],
590
+ region: row["region"],
591
+ awsProfile: row["aws_profile"] || null,
592
+ verified: row["verified"] === 1,
593
+ createdAt: row["created_at"]
594
+ });
595
+ return {
596
+ getHelpers: () => helpers.all().map(rowToHelper),
597
+ getHelper: (id) => {
598
+ const row = helpers.findOne({ where: { id } });
599
+ return row ? rowToHelper(row) : void 0;
600
+ },
601
+ saveHelper: (helper) => {
602
+ const existing = helpers.findOne({ where: { id: helper.id } });
603
+ const row = helperToRow(helper);
604
+ if (existing) {
605
+ helpers.update(row, { where: { id: helper.id } });
606
+ } else {
607
+ helpers.insert(row);
608
+ }
609
+ },
610
+ getS3Profiles: () => s3Profiles.all().map(rowToS3),
611
+ getS3Profile: (id) => {
612
+ const row = s3Profiles.findOne({ where: { id } });
613
+ return row ? rowToS3(row) : void 0;
614
+ },
615
+ saveS3Profile: (profile) => {
616
+ const existing = s3Profiles.findOne({ where: { id: profile.id } });
617
+ const row = { id: profile.id, name: profile.name, bucket: profile.bucket, region: profile.region, aws_profile: profile.awsProfile ?? "", verified: profile.verified ? 1 : 0, created_at: profile.createdAt };
618
+ if (existing) {
619
+ s3Profiles.update(row, { where: { id: profile.id } });
620
+ } else {
621
+ s3Profiles.insert(row);
622
+ }
623
+ },
624
+ deleteS3Profile: (id) => s3Profiles.deleteRows({ where: { id } }),
625
+ getS3Connections: () => s3ConnectionsTable.all().map((row) => rowToS3Connection(row)),
626
+ getS3ConnectionsForProfile: (profileId) => s3ConnectionsTable.findMany({ where: { profile_id: profileId } }).map((row) => rowToS3Connection(row)),
627
+ getS3ConnectionsForAgent: (agentId) => s3ConnectionsTable.findMany({ where: { target_agent: agentId } }).map((row) => rowToS3Connection(row)),
628
+ saveS3Connection: (connection) => {
629
+ const existing = s3ConnectionsTable.findOne({ where: { id: connection.id } });
630
+ const row = s3ConnectionToRow(connection);
631
+ if (existing) {
632
+ s3ConnectionsTable.update(row, { where: { id: connection.id } });
633
+ } else {
634
+ s3ConnectionsTable.insert(row);
635
+ }
636
+ },
637
+ deleteS3Connection: (id) => s3ConnectionsTable.deleteRows({ where: { id } }),
638
+ getDatabaseProfiles: () => dbProfiles.all().map((row) => rowToDb(row)),
639
+ getDatabaseProfile: (id) => {
640
+ const row = dbProfiles.findOne({ where: { id } });
641
+ return row ? rowToDb(row) : void 0;
642
+ },
643
+ saveDatabaseProfile: (profile) => {
644
+ const existing = dbProfiles.findOne({ where: { id: profile.id } });
645
+ const row = {
646
+ id: profile.id,
647
+ name: profile.name,
648
+ db_type: profile.type,
649
+ host: profile.host ?? "",
650
+ port: profile.port ?? 0,
651
+ database: profile.database,
652
+ username: profile.username ?? "",
653
+ password: profile.password ?? "",
654
+ ssl_mode: profile.sslMode ?? "",
655
+ verified: profile.verified ? 1 : 0,
656
+ created_at: profile.createdAt
657
+ };
658
+ if (existing) {
659
+ dbProfiles.update(row, { where: { id: profile.id } });
660
+ } else {
661
+ dbProfiles.insert(row);
662
+ }
663
+ },
664
+ deleteDatabaseProfile: (id) => dbProfiles.deleteRows({ where: { id } }),
665
+ getDatabaseConnections: () => databaseConnectionsTable.all().map((row) => rowToDatabaseConnection(row)),
666
+ getDatabaseConnectionsForProfile: (profileId) => databaseConnectionsTable.findMany({ where: { profile_id: profileId } }).map((row) => rowToDatabaseConnection(row)),
667
+ getDatabaseConnectionsForAgent: (agentId) => databaseConnectionsTable.findMany({ where: { target_agent: agentId } }).map((row) => rowToDatabaseConnection(row)),
668
+ saveDatabaseConnection: (connection) => {
669
+ const existing = databaseConnectionsTable.findOne({ where: { id: connection.id } });
670
+ const row = databaseConnectionToRow(connection);
671
+ if (existing) {
672
+ databaseConnectionsTable.update(row, { where: { id: connection.id } });
673
+ } else {
674
+ databaseConnectionsTable.insert(row);
675
+ }
676
+ },
677
+ deleteDatabaseConnection: (id) => databaseConnectionsTable.deleteRows({ where: { id } }),
678
+ getSetting: (key) => {
679
+ const row = settings.findOne({ where: { key } });
680
+ return row ? row["value"] : void 0;
681
+ },
682
+ setSetting: (key, value) => {
683
+ const existing = settings.findOne({ where: { key } });
684
+ if (existing) {
685
+ settings.update({ key, value }, { where: { key } });
686
+ } else {
687
+ settings.insert({ key, value });
688
+ }
689
+ },
690
+ getEmailAccounts: () => emailAccounts.all().map((row) => rowToEmailAccount(row)),
691
+ getEmailAccount: (id) => {
692
+ const row = emailAccounts.findOne({ where: { id } });
693
+ return row ? rowToEmailAccount(row) : void 0;
694
+ },
695
+ saveEmailAccount: (account) => {
696
+ const existing = emailAccounts.findOne({ where: { id: account.id } });
697
+ const row = emailAccountToRow(account);
698
+ if (existing) {
699
+ emailAccounts.update(row, { where: { id: account.id } });
700
+ } else {
701
+ emailAccounts.insert(row);
702
+ }
703
+ },
704
+ deleteEmailAccount: (id) => {
705
+ emailConnections.deleteRows({ where: { account_id: id } });
706
+ emailAccounts.deleteRows({ where: { id } });
707
+ },
708
+ getEmailConnections: () => emailConnections.all().map((row) => rowToEmailConnection(row)),
709
+ getEmailConnection: (id) => {
710
+ const row = emailConnections.findOne({ where: { id } });
711
+ return row ? rowToEmailConnection(row) : void 0;
712
+ },
713
+ getEmailConnectionsForAccount: (accountId) => emailConnections.findMany({ where: { account_id: accountId } }).map((row) => rowToEmailConnection(row)),
714
+ saveEmailConnection: (connection) => {
715
+ const existing = emailConnections.findOne({ where: { id: connection.id } });
716
+ const row = emailConnectionToRow(connection);
717
+ if (existing) {
718
+ emailConnections.update(row, { where: { id: connection.id } });
719
+ } else {
720
+ emailConnections.insert(row);
721
+ }
722
+ },
723
+ deleteEmailConnection: (id) => emailConnections.deleteRows({ where: { id } }),
724
+ getOAuthCredentials: (provider) => {
725
+ const row = oauthCredentials.findOne({ where: { key: provider } });
726
+ if (!row) return void 0;
727
+ const r = row;
728
+ return { clientId: r["client_id"], clientSecret: r["client_secret"] };
729
+ },
730
+ saveOAuthCredentials: (provider, creds) => {
731
+ const existing = oauthCredentials.findOne({ where: { key: provider } });
732
+ const row = { key: provider, client_id: creds.clientId, client_secret: creds.clientSecret };
733
+ if (existing) {
734
+ oauthCredentials.update(row, { where: { key: provider } });
735
+ } else {
736
+ oauthCredentials.insert(row);
737
+ }
738
+ },
739
+ getTelegramBots: () => telegramBots.all().map((row) => rowToTelegramBot(row)),
740
+ getTelegramBot: (id) => {
741
+ const row = telegramBots.findOne({ where: { id } });
742
+ return row ? rowToTelegramBot(row) : void 0;
743
+ },
744
+ saveTelegramBot: (bot) => {
745
+ const existing = telegramBots.findOne({ where: { id: bot.id } });
746
+ const row = telegramBotToRow(bot);
747
+ if (existing) {
748
+ telegramBots.update(row, { where: { id: bot.id } });
749
+ } else {
750
+ telegramBots.insert(row);
751
+ }
752
+ },
753
+ deleteTelegramBot: (id) => {
754
+ telegramConnections.deleteRows({ where: { bot_id: id } });
755
+ telegramBots.deleteRows({ where: { id } });
756
+ },
757
+ getTelegramConnections: () => telegramConnections.all().map((row) => rowToTelegramConnection(row)),
758
+ getTelegramConnectionsForBot: (botId) => telegramConnections.findMany({ where: { bot_id: botId } }).map((row) => rowToTelegramConnection(row)),
759
+ saveTelegramConnection: (connection) => {
760
+ const existing = telegramConnections.findOne({ where: { id: connection.id } });
761
+ const row = telegramConnectionToRow(connection);
762
+ if (existing) {
763
+ telegramConnections.update(row, { where: { id: connection.id } });
764
+ } else {
765
+ telegramConnections.insert(row);
766
+ }
767
+ },
768
+ deleteTelegramConnection: (id) => telegramConnections.deleteRows({ where: { id } }),
769
+ getHttpEndpoints: () => httpEndpoints.all().map((row) => rowToHttpEndpoint(row)),
770
+ getHttpEndpoint: (id) => {
771
+ const row = httpEndpoints.findOne({ where: { id } });
772
+ return row ? rowToHttpEndpoint(row) : void 0;
773
+ },
774
+ getHttpEndpointByPath: (path) => {
775
+ const row = httpEndpoints.findOne({ where: { path } });
776
+ return row ? rowToHttpEndpoint(row) : void 0;
777
+ },
778
+ saveHttpEndpoint: (endpoint) => {
779
+ const existing = httpEndpoints.findOne({ where: { id: endpoint.id } });
780
+ const row = httpEndpointToRow(endpoint);
781
+ if (existing) {
782
+ httpEndpoints.update(row, { where: { id: endpoint.id } });
783
+ } else {
784
+ httpEndpoints.insert(row);
785
+ }
786
+ },
787
+ deleteHttpEndpoint: (id) => {
788
+ httpConnectionsTable.deleteRows({ where: { endpoint_id: id } });
789
+ httpEndpoints.deleteRows({ where: { id } });
790
+ },
791
+ getHttpConnections: () => httpConnectionsTable.all().map((row) => rowToHttpConnection(row)),
792
+ getHttpConnectionsForEndpoint: (endpointId) => httpConnectionsTable.findMany({ where: { endpoint_id: endpointId } }).map((row) => rowToHttpConnection(row)),
793
+ saveHttpConnection: (connection) => {
794
+ const existing = httpConnectionsTable.findOne({ where: { id: connection.id } });
795
+ const row = httpConnectionToRow(connection);
796
+ if (existing) {
797
+ httpConnectionsTable.update(row, { where: { id: connection.id } });
798
+ } else {
799
+ httpConnectionsTable.insert(row);
800
+ }
801
+ },
802
+ deleteHttpConnection: (id) => httpConnectionsTable.deleteRows({ where: { id } }),
803
+ getCronJobs: () => cronJobsTable.all().map((row) => rowToCronJob(row)),
804
+ getCronJob: (id) => {
805
+ const row = cronJobsTable.findOne({ where: { id } });
806
+ return row ? rowToCronJob(row) : void 0;
807
+ },
808
+ saveCronJob: (job) => {
809
+ const existing = cronJobsTable.findOne({ where: { id: job.id } });
810
+ const row = cronJobToRow(job);
811
+ if (existing) {
812
+ cronJobsTable.update(row, { where: { id: job.id } });
813
+ } else {
814
+ cronJobsTable.insert(row);
815
+ }
816
+ },
817
+ deleteCronJob: (id) => {
818
+ cronConnectionsTable.deleteRows({ where: { cron_job_id: id } });
819
+ cronJobsTable.deleteRows({ where: { id } });
820
+ },
821
+ getCronConnections: () => cronConnectionsTable.all().map((row) => rowToCronConnection(row)),
822
+ getCronConnectionsForJob: (cronJobId) => cronConnectionsTable.findMany({ where: { cron_job_id: cronJobId } }).map((row) => rowToCronConnection(row)),
823
+ saveCronConnection: (connection) => {
824
+ const existing = cronConnectionsTable.findOne({ where: { id: connection.id } });
825
+ const row = cronConnectionToRow(connection);
826
+ if (existing) {
827
+ cronConnectionsTable.update(row, { where: { id: connection.id } });
828
+ } else {
829
+ cronConnectionsTable.insert(row);
830
+ }
831
+ },
832
+ deleteCronConnection: (id) => cronConnectionsTable.deleteRows({ where: { id } }),
833
+ close: () => store.close()
834
+ };
835
+ };
836
+
837
+ // ../daemon/src/process-manager.ts
838
+ import { fork } from "child_process";
839
+ import { resolve, delimiter } from "path";
840
+ var MAX_RESTARTS = 10;
841
+ var BASE_RESTART_DELAY = 1e3;
842
+ var MAX_RESTART_DELAY = 6e4;
843
+ var HEALTH_CHECK_INTERVAL = 3e4;
844
+ var resolveEntryScript = (runnersDir, isDev, type, helperKind) => {
845
+ const runnerMap = {
846
+ agent: { dev: "agent-runner/src/entry.ts", cli: "agent-runner.js" },
847
+ cron: { dev: "cron-runner/src/entry.ts", cli: "cron-runner.js" },
848
+ "email-imap": { dev: "imap-runner/src/entry.ts", cli: "imap-runner.js" },
849
+ telegram: { dev: "telegram-runner/src/entry.ts", cli: "telegram-runner.js" }
850
+ };
851
+ const key = type === "agent" ? "agent" : helperKind;
852
+ const entry = key ? runnerMap[key] : void 0;
853
+ if (!entry) throw new Error(`Unknown helper kind: ${helperKind}`);
854
+ return resolve(runnersDir, isDev ? entry.dev : entry.cli);
855
+ };
856
+ var createProcessManager = (options) => {
857
+ const { hubId, runnersDir, isDev, callbacks } = options;
858
+ const processes = /* @__PURE__ */ new Map();
859
+ const setStatus = (id, status, pid) => {
860
+ const record = processes.get(id);
861
+ if (!record) return;
862
+ record.info.status = status;
863
+ if (pid !== void 0) record.info.pid = pid;
864
+ callbacks?.onStatusChange?.(id, status, pid);
865
+ };
866
+ const restartDelay = (restartCount) => Math.min(BASE_RESTART_DELAY * 2 ** restartCount, MAX_RESTART_DELAY);
867
+ const startHealthCheck = (id) => {
868
+ const record = processes.get(id);
869
+ if (!record?.child) return;
870
+ record.healthInterval = setInterval(() => {
871
+ if (record.awaitingPong) {
872
+ console.log(`[process-manager] ${id} failed health check, restarting...`);
873
+ record.child?.kill("SIGTERM");
874
+ return;
875
+ }
876
+ record.awaitingPong = true;
877
+ record.child?.send({ type: "ping" });
878
+ }, HEALTH_CHECK_INTERVAL);
879
+ };
880
+ const stopHealthCheck = (id) => {
881
+ const record = processes.get(id);
882
+ if (record?.healthInterval) {
883
+ clearInterval(record.healthInterval);
884
+ record.healthInterval = void 0;
885
+ }
886
+ };
887
+ const forkProcess = (id, record) => {
888
+ const helperKind = record.info.type === "helper" ? record.info.helperKind : void 0;
889
+ const entryScript = resolveEntryScript(runnersDir, isDev, record.info.type, helperKind);
890
+ setStatus(id, "starting");
891
+ const execArgv = isDev ? ["--import", "tsx"] : [];
892
+ const globalNodeModules = isDev ? "" : resolve(runnersDir, "..");
893
+ const nodePath = [process.env["NODE_PATH"], globalNodeModules].filter(Boolean).join(delimiter);
894
+ const child = fork(entryScript, [], {
895
+ execArgv,
896
+ env: {
897
+ ...process.env,
898
+ KINDFLOW_PROCESS_ID: id,
899
+ KINDFLOW_HUB_ID: hubId,
900
+ NODE_PATH: nodePath
901
+ },
902
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
903
+ });
904
+ record.child = child;
905
+ record.info.pid = child.pid;
906
+ record.info.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
907
+ child.stdout?.on("data", (data) => {
908
+ callbacks?.onLog?.(id, "stdout", data.toString());
909
+ });
910
+ child.stderr?.on("data", (data) => {
911
+ callbacks?.onLog?.(id, "stderr", data.toString());
912
+ });
913
+ child.on("message", (msg) => {
914
+ switch (msg.type) {
915
+ case "ready":
916
+ setStatus(id, "running", child.pid);
917
+ startHealthCheck(id);
918
+ console.log(`[process-manager] ${id} is ready (PID ${child.pid})`);
919
+ break;
920
+ case "pong":
921
+ record.awaitingPong = false;
922
+ break;
923
+ case "agent:sdk-message":
924
+ callbacks?.onSdkMessage?.(id, msg.message);
925
+ break;
926
+ case "log":
927
+ callbacks?.onLog?.(id, "stdout", `[${msg.level}] ${msg.message}`);
928
+ break;
929
+ }
930
+ });
931
+ child.on("exit", (code) => {
932
+ stopHealthCheck(id);
933
+ record.child = void 0;
934
+ record.info.pid = void 0;
935
+ record.info.lastStoppedAt = (/* @__PURE__ */ new Date()).toISOString();
936
+ if (record.info.status === "stopping" || record.info.status === "stopped") {
937
+ setStatus(id, "stopped");
938
+ return;
939
+ }
940
+ record.info.restartCount++;
941
+ if (record.info.restartCount > MAX_RESTARTS) {
942
+ console.log(`[process-manager] ${id} crashed too many times (${MAX_RESTARTS}), giving up`);
943
+ setStatus(id, "crashed");
944
+ return;
945
+ }
946
+ const delay = restartDelay(record.info.restartCount);
947
+ console.log(`[process-manager] ${id} exited (code ${code}), restarting in ${delay}ms (attempt ${record.info.restartCount}/${MAX_RESTARTS})`);
948
+ setTimeout(() => forkProcess(id, record), delay);
949
+ });
950
+ child.send({ type: "config", config: record.config });
951
+ };
952
+ return {
953
+ start(id, type, config) {
954
+ if (processes.has(id)) {
955
+ console.log(`[process-manager] ${id} already exists, stopping first`);
956
+ this.stop(id);
957
+ }
958
+ const record = {
959
+ info: {
960
+ id,
961
+ type,
962
+ helperKind: type === "helper" ? config.kind : void 0,
963
+ status: "stopped",
964
+ restartCount: 0
965
+ },
966
+ config,
967
+ awaitingPong: false
968
+ };
969
+ processes.set(id, record);
970
+ forkProcess(id, record);
971
+ },
972
+ async stop(id) {
973
+ const record = processes.get(id);
974
+ if (!record?.child) return;
975
+ setStatus(id, "stopping");
976
+ stopHealthCheck(id);
977
+ record.child.send({ type: "stop" });
978
+ await new Promise((resolve5) => {
979
+ const timeout = setTimeout(() => {
980
+ record.child?.kill("SIGKILL");
981
+ resolve5();
982
+ }, 5e3);
983
+ record.child?.on("exit", () => {
984
+ clearTimeout(timeout);
985
+ resolve5();
986
+ });
987
+ });
988
+ setStatus(id, "stopped");
989
+ },
990
+ async restart(id) {
991
+ const record = processes.get(id);
992
+ if (!record) return;
993
+ await this.stop(id);
994
+ record.info.restartCount = 0;
995
+ forkProcess(id, record);
996
+ },
997
+ getAll: () => [...processes.values()].map((r) => ({ ...r.info })),
998
+ get: (id) => {
999
+ const record = processes.get(id);
1000
+ return record ? { ...record.info } : void 0;
1001
+ },
1002
+ sendToChild(id, msg) {
1003
+ const record = processes.get(id);
1004
+ record?.child?.send(msg);
1005
+ },
1006
+ async shutdownAll() {
1007
+ console.log(`[process-manager] Shutting down ${processes.size} process(es)...`);
1008
+ const stopPromises = [...processes.keys()].map((id) => this.stop(id));
1009
+ await Promise.all(stopPromises);
1010
+ console.log("[process-manager] All processes stopped");
1011
+ }
1012
+ };
1013
+ };
1014
+
1015
+ // ../daemon/src/terminal-manager.ts
1016
+ import { execSync } from "child_process";
1017
+ import * as pty from "node-pty";
1018
+ import { listSessions } from "@anthropic-ai/claude-agent-sdk";
1019
+ var resolveClaudePath = () => {
1020
+ try {
1021
+ return execSync("which claude", { encoding: "utf-8" }).trim();
1022
+ } catch {
1023
+ return "claude";
1024
+ }
1025
+ };
1026
+ var createTerminalManager = (options) => {
1027
+ const sessions = /* @__PURE__ */ new Map();
1028
+ const idleTimeout = options.idleTimeoutMs ?? 10 * 60 * 1e3;
1029
+ const resetIdleTimer = (sessionId) => {
1030
+ const session = sessions.get(sessionId);
1031
+ if (!session) return;
1032
+ if (session.idleTimer) clearTimeout(session.idleTimer);
1033
+ session.idleTimer = setTimeout(() => {
1034
+ console.log(`[terminal] Session ${sessionId} idle timeout, closing`);
1035
+ manager.close(sessionId);
1036
+ }, idleTimeout);
1037
+ };
1038
+ const manager = {
1039
+ async open(sessionId, agentId, agentCwd) {
1040
+ const existing = this.getByAgent(agentId);
1041
+ if (existing) this.close(existing.id);
1042
+ const previousSessions = await listSessions({ dir: agentCwd });
1043
+ const hasPreviousSession = previousSessions.length > 0;
1044
+ const claudePath = resolveClaudePath();
1045
+ const shell = process.env["SHELL"] ?? "/bin/bash";
1046
+ const continueFlag = hasPreviousSession ? " '--continue'" : "";
1047
+ const fullCommand = `'${claudePath}' '--permission-mode' 'acceptEdits'${continueFlag}`;
1048
+ console.log(`[terminal] ${hasPreviousSession ? "Resuming" : "Starting new"} session for ${agentId} (cwd: ${agentCwd})`);
1049
+ const ptyProcess = pty.spawn(shell, ["-l", "-c", fullCommand], {
1050
+ name: "xterm-256color",
1051
+ cols: 120,
1052
+ rows: 30,
1053
+ cwd: agentCwd,
1054
+ env: { ...process.env }
1055
+ });
1056
+ const session = {
1057
+ info: {
1058
+ id: sessionId,
1059
+ agentId,
1060
+ status: "running",
1061
+ lastActivity: /* @__PURE__ */ new Date()
1062
+ },
1063
+ ptyProcess,
1064
+ outputBuffer: [],
1065
+ flushed: false
1066
+ };
1067
+ sessions.set(sessionId, session);
1068
+ let helloSent = hasPreviousSession;
1069
+ ptyProcess.onData((data) => {
1070
+ session.info.lastActivity = /* @__PURE__ */ new Date();
1071
+ if (!helloSent && data.includes("accept edits")) {
1072
+ helloSent = true;
1073
+ setTimeout(() => {
1074
+ if (session.info.status === "running") {
1075
+ ptyProcess.write("hello\r");
1076
+ }
1077
+ }, 500);
1078
+ }
1079
+ if (session.flushed) {
1080
+ options.callbacks?.onOutput?.(sessionId, data);
1081
+ } else {
1082
+ session.outputBuffer.push(data);
1083
+ }
1084
+ });
1085
+ ptyProcess.onExit(({ exitCode }) => {
1086
+ console.log(`[terminal] PTY exited for session ${sessionId} (code ${exitCode})`);
1087
+ manager.close(sessionId);
1088
+ });
1089
+ resetIdleTimer(sessionId);
1090
+ console.log(`[terminal] Opened PTY session ${sessionId} for agent ${agentId} (cwd: ${agentCwd})`);
1091
+ return session.info;
1092
+ },
1093
+ flush(sessionId) {
1094
+ const session = sessions.get(sessionId);
1095
+ if (!session || session.flushed) return;
1096
+ session.flushed = true;
1097
+ console.log(`[terminal] Flushing ${session.outputBuffer.length} buffered chunks for session ${sessionId}`);
1098
+ for (const buffered of session.outputBuffer) {
1099
+ options.callbacks?.onOutput?.(sessionId, buffered);
1100
+ }
1101
+ session.outputBuffer = [];
1102
+ },
1103
+ write(sessionId, data) {
1104
+ const session = sessions.get(sessionId);
1105
+ if (!session || session.info.status !== "running") return;
1106
+ if (!session.flushed) this.flush(sessionId);
1107
+ session.info.lastActivity = /* @__PURE__ */ new Date();
1108
+ resetIdleTimer(sessionId);
1109
+ session.ptyProcess.write(data);
1110
+ },
1111
+ resize(sessionId, cols, rows) {
1112
+ const session = sessions.get(sessionId);
1113
+ if (!session || session.info.status !== "running") return;
1114
+ session.ptyProcess.resize(cols, rows);
1115
+ },
1116
+ close(sessionId) {
1117
+ const session = sessions.get(sessionId);
1118
+ if (!session) return;
1119
+ session.info.status = "closed";
1120
+ if (session.idleTimer) clearTimeout(session.idleTimer);
1121
+ try {
1122
+ session.ptyProcess.kill();
1123
+ } catch {
1124
+ }
1125
+ sessions.delete(sessionId);
1126
+ options.callbacks?.onClose?.(sessionId);
1127
+ console.log(`[terminal] Closed session ${sessionId}`);
1128
+ },
1129
+ get(sessionId) {
1130
+ return sessions.get(sessionId)?.info;
1131
+ },
1132
+ getByAgent(agentId) {
1133
+ for (const session of sessions.values()) {
1134
+ if (session.info.agentId === agentId) return session.info;
1135
+ }
1136
+ return void 0;
1137
+ },
1138
+ closeAll() {
1139
+ for (const sessionId of [...sessions.keys()]) {
1140
+ this.close(sessionId);
1141
+ }
1142
+ }
1143
+ };
1144
+ return manager;
1145
+ };
1146
+
1147
+ // ../daemon/src/web-server.ts
1148
+ import { fork as fork2 } from "child_process";
1149
+ import { resolve as resolve2 } from "path";
1150
+ import { existsSync } from "fs";
1151
+ var createWebServer = (options) => {
1152
+ let child = null;
1153
+ const candidates = [
1154
+ resolve2(options.webAppDir, "standalone", "server.js"),
1155
+ resolve2(options.webAppDir, "standalone", "apps", "web", "server.js"),
1156
+ resolve2(options.webAppDir, ".next", "standalone", "server.js"),
1157
+ resolve2(options.webAppDir, ".next", "standalone", "apps", "web", "server.js")
1158
+ ];
1159
+ const standaloneServer = candidates.find((p) => existsSync(p));
1160
+ return {
1161
+ start() {
1162
+ return new Promise((resolvePromise, reject) => {
1163
+ if (!standaloneServer) {
1164
+ reject(new Error(
1165
+ `No standalone server.js found. Run "npm run build --workspace=apps/web" first.
1166
+ Looked in:
1167
+ ${candidates.join("\n ")}`
1168
+ ));
1169
+ return;
1170
+ }
1171
+ child = fork2(standaloneServer, [], {
1172
+ env: {
1173
+ ...process.env,
1174
+ PORT: String(options.port),
1175
+ HOSTNAME: "0.0.0.0"
1176
+ },
1177
+ stdio: ["ignore", "pipe", "pipe", "ipc"]
1178
+ });
1179
+ let started = false;
1180
+ child.stdout?.on("data", (data) => {
1181
+ const line = data.toString();
1182
+ process.stdout.write(`[web] ${line}`);
1183
+ if (!started && (line.includes("Ready") || line.includes("Listening"))) {
1184
+ started = true;
1185
+ resolvePromise();
1186
+ }
1187
+ });
1188
+ child.stderr?.on("data", (data) => {
1189
+ process.stderr.write(`[web] ${data.toString()}`);
1190
+ });
1191
+ child.on("error", (err) => {
1192
+ console.error(`[web] Failed to start: ${err.message}`);
1193
+ if (!started) reject(err);
1194
+ });
1195
+ child.on("exit", (code) => {
1196
+ console.log(`[web] Process exited (code ${code})`);
1197
+ child = null;
1198
+ if (!started) reject(new Error(`Web server exited with code ${code}`));
1199
+ });
1200
+ setTimeout(() => {
1201
+ if (!started) {
1202
+ started = true;
1203
+ console.log("[web] Startup timeout \u2014 assuming ready");
1204
+ resolvePromise();
1205
+ }
1206
+ }, 3e4);
1207
+ });
1208
+ },
1209
+ stop() {
1210
+ if (child) {
1211
+ console.log("[web] Stopping web server...");
1212
+ child.kill("SIGTERM");
1213
+ setTimeout(() => {
1214
+ if (child) {
1215
+ child.kill("SIGKILL");
1216
+ child = null;
1217
+ }
1218
+ }, 5e3);
1219
+ }
1220
+ },
1221
+ isRunning() {
1222
+ return child !== null;
1223
+ }
1224
+ };
1225
+ };
1226
+
1227
+ // ../daemon/src/api/server.ts
1228
+ import { createServer } from "http";
1229
+ import { WebSocketServer } from "ws";
1230
+ var parsePath = (path) => {
1231
+ const paramNames = [];
1232
+ const regexStr = path.replace(/:([^/]+)/g, (_, name) => {
1233
+ paramNames.push(name);
1234
+ return "([^/]+)";
1235
+ });
1236
+ return { pattern: new RegExp(`^${regexStr}$`), paramNames };
1237
+ };
1238
+ var readBody = (req) => new Promise((resolve5) => {
1239
+ const chunks = [];
1240
+ req.on("data", (chunk) => chunks.push(chunk));
1241
+ req.on("end", () => {
1242
+ const raw = Buffer.concat(chunks).toString();
1243
+ if (!raw) return resolve5(null);
1244
+ try {
1245
+ resolve5(JSON.parse(raw));
1246
+ } catch {
1247
+ resolve5(null);
1248
+ }
1249
+ });
1250
+ });
1251
+ var sendJson = (res, statusCode, body) => {
1252
+ const json = JSON.stringify(body);
1253
+ res.writeHead(statusCode, {
1254
+ "Content-Type": "application/json",
1255
+ "Access-Control-Allow-Origin": "*",
1256
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1257
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
1258
+ });
1259
+ res.end(json);
1260
+ };
1261
+ var createApiServer = () => {
1262
+ const routes = [];
1263
+ const wsClients = /* @__PURE__ */ new Set();
1264
+ let wsCommandHandler = null;
1265
+ const httpServer = createServer(async (req, res) => {
1266
+ if (req.method === "OPTIONS") {
1267
+ res.writeHead(204, {
1268
+ "Access-Control-Allow-Origin": "*",
1269
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1270
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
1271
+ });
1272
+ res.end();
1273
+ return;
1274
+ }
1275
+ const url = req.url ?? "/";
1276
+ const [path] = url.split("?");
1277
+ const method = req.method ?? "GET";
1278
+ for (const route of routes) {
1279
+ if (route.method !== method) continue;
1280
+ const match = path?.match(route.pattern);
1281
+ if (!match) continue;
1282
+ const params = {};
1283
+ route.paramNames.forEach((name, i) => {
1284
+ params[name] = match[i + 1] ?? "";
1285
+ });
1286
+ const body = await readBody(req);
1287
+ try {
1288
+ await route.handler(req, res, params, body);
1289
+ } catch (err) {
1290
+ console.error(`[api] Error handling ${method} ${path}:`, err);
1291
+ sendJson(res, 500, { error: "INTERNAL_ERROR", message: "Internal server error", statusCode: 500 });
1292
+ }
1293
+ return;
1294
+ }
1295
+ sendJson(res, 404, { error: "NOT_FOUND", message: `No route for ${method} ${path}`, statusCode: 404 });
1296
+ });
1297
+ const wss = new WebSocketServer({ server: httpServer });
1298
+ wss.on("error", () => {
1299
+ });
1300
+ wss.on("connection", (ws) => {
1301
+ wsClients.add(ws);
1302
+ ws.on("message", (data) => {
1303
+ try {
1304
+ const command = JSON.parse(data.toString());
1305
+ wsCommandHandler?.(ws, command);
1306
+ } catch {
1307
+ }
1308
+ });
1309
+ ws.on("close", () => {
1310
+ wsClients.delete(ws);
1311
+ });
1312
+ });
1313
+ return {
1314
+ listen(port) {
1315
+ return new Promise((resolve5, reject) => {
1316
+ httpServer.once("error", (err) => {
1317
+ if (err.code === "EADDRINUSE") {
1318
+ reject(new Error(`Port ${port} is already in use. Stop the existing process or use --port to pick another.`));
1319
+ } else {
1320
+ reject(err);
1321
+ }
1322
+ });
1323
+ httpServer.listen(port, () => {
1324
+ console.log(`[api] HTTP + WebSocket server listening on port ${port}`);
1325
+ resolve5();
1326
+ });
1327
+ });
1328
+ },
1329
+ route(method, path, handler) {
1330
+ const { pattern, paramNames } = parsePath(path);
1331
+ routes.push({ method, pattern, paramNames, handler });
1332
+ },
1333
+ onWsCommand(handler) {
1334
+ wsCommandHandler = handler;
1335
+ },
1336
+ broadcast(event) {
1337
+ const data = JSON.stringify(event);
1338
+ for (const ws of wsClients) {
1339
+ if (ws.readyState === ws.OPEN) {
1340
+ ws.send(data);
1341
+ }
1342
+ }
1343
+ },
1344
+ close() {
1345
+ return new Promise((resolve5) => {
1346
+ wss.close();
1347
+ httpServer.close(() => resolve5());
1348
+ });
1349
+ }
1350
+ };
1351
+ };
1352
+
1353
+ // ../daemon/src/api/routes/agents.ts
1354
+ import { randomUUID } from "crypto";
1355
+ import { resolve as resolve3 } from "path";
1356
+ var cascadeDeleteAgentConnections = (configStore, agentId) => {
1357
+ for (const conn of configStore.getCronConnections()) {
1358
+ if (conn.targetAgent === agentId) configStore.deleteCronConnection(conn.id);
1359
+ }
1360
+ for (const conn of configStore.getHttpConnections()) {
1361
+ if (conn.targetAgent === agentId) configStore.deleteHttpConnection(conn.id);
1362
+ }
1363
+ for (const conn of configStore.getEmailConnections()) {
1364
+ if (conn.targetAgent === agentId) configStore.deleteEmailConnection(conn.id);
1365
+ }
1366
+ for (const conn of configStore.getTelegramConnections()) {
1367
+ if (conn.targetAgent === agentId) configStore.deleteTelegramConnection(conn.id);
1368
+ }
1369
+ for (const conn of configStore.getS3Connections()) {
1370
+ if (conn.targetAgent === agentId) configStore.deleteS3Connection(conn.id);
1371
+ }
1372
+ for (const conn of configStore.getDatabaseConnections()) {
1373
+ if (conn.targetAgent === agentId) configStore.deleteDatabaseConnection(conn.id);
1374
+ }
1375
+ };
1376
+ var registerAgentRoutes = (server, processManager, terminalManager, agentsDir, templateDir, configStore) => {
1377
+ server.route("GET", "/api/agents", async (_req, res) => {
1378
+ const agents = scanAgents(agentsDir);
1379
+ sendJson(res, 200, { agents });
1380
+ });
1381
+ server.route("GET", "/api/agents/:id", async (_req, res, params) => {
1382
+ const agent = readAgent(agentsDir, params["id"]);
1383
+ if (!agent) {
1384
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Agent not found", statusCode: 404 });
1385
+ return;
1386
+ }
1387
+ sendJson(res, 200, agent);
1388
+ });
1389
+ server.route("POST", "/api/agents", async (_req, res, _params, body) => {
1390
+ const input = body;
1391
+ if (!input?.name) {
1392
+ sendJson(res, 400, { error: "BAD_REQUEST", message: "name is required", statusCode: 400 });
1393
+ return;
1394
+ }
1395
+ const slug = generateSlug(input.name);
1396
+ const id = slug || randomUUID().slice(0, 8);
1397
+ const agent = createAgentFromTemplate(agentsDir, templateDir, id, input.name, input.autoStart ?? true);
1398
+ sendJson(res, 201, agent);
1399
+ });
1400
+ server.route("PUT", "/api/agents/:id", async (_req, res, params, body) => {
1401
+ const agent = readAgent(agentsDir, params["id"]);
1402
+ if (!agent) {
1403
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Agent not found", statusCode: 404 });
1404
+ return;
1405
+ }
1406
+ const updates = body;
1407
+ const updated = { ...agent.config, ...updates };
1408
+ writeAgentConfig(agentsDir, params["id"], updated);
1409
+ const refreshed = readAgent(agentsDir, params["id"]);
1410
+ sendJson(res, 200, refreshed);
1411
+ });
1412
+ server.route("DELETE", "/api/agents/:id", async (_req, res, params, body) => {
1413
+ const id = params["id"];
1414
+ const agent = readAgent(agentsDir, id);
1415
+ if (!agent) {
1416
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Agent not found", statusCode: 404 });
1417
+ return;
1418
+ }
1419
+ const { confirm } = body ?? {};
1420
+ if (!confirm) {
1421
+ sendJson(res, 200, {
1422
+ confirm: false,
1423
+ willDelete: {
1424
+ agent: agent.config.name,
1425
+ agentDir: agent.dir,
1426
+ process: processManager.get(id)?.status ?? null,
1427
+ terminalSession: terminalManager.getByAgent(id)?.id ?? null
1428
+ }
1429
+ });
1430
+ return;
1431
+ }
1432
+ const session = terminalManager.getByAgent(id);
1433
+ if (session) terminalManager.close(session.id);
1434
+ const proc = processManager.get(id);
1435
+ if (proc && (proc.status === "running" || proc.status === "starting")) {
1436
+ await processManager.stop(id);
1437
+ }
1438
+ cascadeDeleteAgentConnections(configStore, id);
1439
+ deleteAgentDir(agentsDir, id);
1440
+ sendJson(res, 200, { deleted: true });
1441
+ });
1442
+ server.route("POST", "/api/agents/:id/message", async (_req, res, params, body) => {
1443
+ const id = params["id"];
1444
+ const proc = processManager.get(id);
1445
+ if (!proc || proc.status !== "running") {
1446
+ sendJson(res, 400, { error: "NOT_RUNNING", message: "Agent is not running", statusCode: 400 });
1447
+ return;
1448
+ }
1449
+ const { text } = body;
1450
+ if (!text) {
1451
+ sendJson(res, 400, { error: "BAD_REQUEST", message: "text is required", statusCode: 400 });
1452
+ return;
1453
+ }
1454
+ sendJson(res, 200, { sent: true });
1455
+ });
1456
+ server.route("GET", "/api/agents/:id/commands", async (_req, res, params) => {
1457
+ const id = params["id"];
1458
+ const agent = readAgent(agentsDir, id);
1459
+ if (!agent) {
1460
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Agent not found", statusCode: 404 });
1461
+ return;
1462
+ }
1463
+ const commands = listAgentCommands(agentsDir, id);
1464
+ sendJson(res, 200, { commands });
1465
+ });
1466
+ server.route("POST", "/api/agents/:id/terminal", async (_req, res, params) => {
1467
+ const id = params["id"];
1468
+ const agent = readAgent(agentsDir, id);
1469
+ if (!agent) {
1470
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Agent not found", statusCode: 404 });
1471
+ return;
1472
+ }
1473
+ const sessionId = randomUUID();
1474
+ const masterCwd = resolve3(agent.dir, "master");
1475
+ await terminalManager.open(sessionId, id, masterCwd);
1476
+ sendJson(res, 200, { sessionId });
1477
+ });
1478
+ server.route("DELETE", "/api/agents/:id/terminal", async (_req, res, params) => {
1479
+ const id = params["id"];
1480
+ const session = terminalManager.getByAgent(id);
1481
+ if (!session) {
1482
+ sendJson(res, 404, { error: "NOT_FOUND", message: "No terminal session for this agent", statusCode: 404 });
1483
+ return;
1484
+ }
1485
+ terminalManager.close(session.id);
1486
+ sendJson(res, 200, { closed: true });
1487
+ });
1488
+ };
1489
+
1490
+ // ../daemon/src/api/routes/processes.ts
1491
+ var registerProcessRoutes = (server, processManager, configStore, agentsDir) => {
1492
+ server.route("GET", "/api/processes", async (_req, res) => {
1493
+ sendJson(res, 200, { processes: processManager.getAll() });
1494
+ });
1495
+ server.route("GET", "/api/processes/:id", async (_req, res, params) => {
1496
+ const proc = processManager.get(params["id"]);
1497
+ if (!proc) {
1498
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Process not found", statusCode: 404 });
1499
+ return;
1500
+ }
1501
+ sendJson(res, 200, proc);
1502
+ });
1503
+ server.route("POST", "/api/processes/:id/start", async (_req, res, params) => {
1504
+ const id = params["id"];
1505
+ const agent = readAgent(agentsDir, id);
1506
+ if (agent) {
1507
+ processManager.start(id, "agent", agent);
1508
+ sendJson(res, 200, { started: true });
1509
+ return;
1510
+ }
1511
+ const helper = configStore.getHelper(id);
1512
+ if (helper) {
1513
+ processManager.start(id, "helper", helper);
1514
+ sendJson(res, 200, { started: true });
1515
+ return;
1516
+ }
1517
+ sendJson(res, 404, { error: "NOT_FOUND", message: "No agent or helper with this ID", statusCode: 404 });
1518
+ });
1519
+ server.route("POST", "/api/processes/:id/stop", async (_req, res, params) => {
1520
+ const proc = processManager.get(params["id"]);
1521
+ if (!proc) {
1522
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Process not found", statusCode: 404 });
1523
+ return;
1524
+ }
1525
+ await processManager.stop(params["id"]);
1526
+ sendJson(res, 200, { stopped: true });
1527
+ });
1528
+ server.route("POST", "/api/processes/:id/restart", async (_req, res, params) => {
1529
+ const proc = processManager.get(params["id"]);
1530
+ if (!proc) {
1531
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Process not found", statusCode: 404 });
1532
+ return;
1533
+ }
1534
+ await processManager.restart(params["id"]);
1535
+ sendJson(res, 200, { restarted: true });
1536
+ });
1537
+ };
1538
+
1539
+ // ../daemon/src/api/routes/s3.ts
1540
+ import { randomUUID as randomUUID2 } from "crypto";
1541
+
1542
+ // ../daemon/src/capability-files.ts
1543
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, unlinkSync, rmSync, existsSync as existsSync2, readdirSync } from "fs";
1544
+ import { resolve as resolve4, dirname as dirname2 } from "path";
1545
+ var generateCapabilityFile = (agentsDir, agentId, capabilityType, connectionId, content) => {
1546
+ const filePath = resolve4(agentsDir, agentId, "agent", "capabilities", capabilityType, `${connectionId}.md`);
1547
+ mkdirSync2(dirname2(filePath), { recursive: true });
1548
+ writeFileSync2(filePath, content, "utf-8");
1549
+ };
1550
+ var removeCapabilityFile = (agentsDir, agentId, capabilityType, connectionId) => {
1551
+ const filePath = resolve4(agentsDir, agentId, "agent", "capabilities", capabilityType, `${connectionId}.md`);
1552
+ try {
1553
+ unlinkSync(filePath);
1554
+ } catch {
1555
+ }
1556
+ };
1557
+
1558
+ // ../daemon/src/api/routes/s3.ts
1559
+ var verifyProfile = async (profile) => {
1560
+ const bucket = createS3Bucket({
1561
+ bucket: profile.bucket,
1562
+ region: profile.region,
1563
+ profile: profile.awsProfile ?? void 0
1564
+ });
1565
+ return bucket.verify();
1566
+ };
1567
+ var buildS3CapabilityContent = (profile, connection) => {
1568
+ const profileName = profile.awsProfile ? `'${profile.awsProfile}'` : "undefined";
1569
+ return [
1570
+ `# S3: ${profile.name}`,
1571
+ `Bucket: ${profile.bucket}`,
1572
+ `Region: ${profile.region}`,
1573
+ profile.awsProfile ? `AWS Profile: ${profile.awsProfile}` : null,
1574
+ "",
1575
+ "## Usage",
1576
+ connection.purpose,
1577
+ "",
1578
+ "## Code",
1579
+ "```ts",
1580
+ "import { createS3Bucket } from 'kindflow/s3';",
1581
+ "const bucket = createS3Bucket({",
1582
+ ` bucket: '${profile.bucket}',`,
1583
+ ` region: '${profile.region}',`,
1584
+ ` profile: ${profileName}`,
1585
+ "});",
1586
+ "```"
1587
+ ].filter((line) => line !== null).join("\n");
1588
+ };
1589
+ var registerS3Routes = (server, configStore, agentsDir) => {
1590
+ server.route("GET", "/api/s3/profiles", async (_req, res) => {
1591
+ sendJson(res, 200, { profiles: configStore.getS3Profiles() });
1592
+ });
1593
+ server.route("GET", "/api/s3/profiles/:id", async (_req, res, params) => {
1594
+ const profile = configStore.getS3Profile(params["id"]);
1595
+ if (!profile) {
1596
+ sendJson(res, 404, { error: "NOT_FOUND", message: "S3 profile not found", statusCode: 404 });
1597
+ return;
1598
+ }
1599
+ sendJson(res, 200, profile);
1600
+ });
1601
+ server.route("POST", "/api/s3/profiles", async (_req, res, _params, body) => {
1602
+ const input = body;
1603
+ if (!input?.name || !input?.bucket || !input?.region) {
1604
+ sendJson(res, 400, { error: "BAD_REQUEST", message: "name, bucket, and region are required", statusCode: 400 });
1605
+ return;
1606
+ }
1607
+ const profile = {
1608
+ id: input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || randomUUID2().slice(0, 8),
1609
+ name: input.name,
1610
+ bucket: input.bucket,
1611
+ region: input.region,
1612
+ awsProfile: input.awsProfile ?? null,
1613
+ verified: false,
1614
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1615
+ };
1616
+ const error = await verifyProfile(profile);
1617
+ if (error) {
1618
+ sendJson(res, 400, { error: "VERIFICATION_FAILED", message: error, statusCode: 400 });
1619
+ return;
1620
+ }
1621
+ profile.verified = true;
1622
+ configStore.saveS3Profile(profile);
1623
+ sendJson(res, 201, profile);
1624
+ });
1625
+ server.route("POST", "/api/s3/profiles/:id/verify", async (_req, res, params) => {
1626
+ const profile = configStore.getS3Profile(params["id"]);
1627
+ if (!profile) {
1628
+ sendJson(res, 404, { error: "NOT_FOUND", message: "S3 profile not found", statusCode: 404 });
1629
+ return;
1630
+ }
1631
+ const error = await verifyProfile(profile);
1632
+ if (error) {
1633
+ profile.verified = false;
1634
+ configStore.saveS3Profile(profile);
1635
+ sendJson(res, 200, { verified: false, error });
1636
+ return;
1637
+ }
1638
+ profile.verified = true;
1639
+ configStore.saveS3Profile(profile);
1640
+ sendJson(res, 200, { verified: true });
1641
+ });
1642
+ server.route("PUT", "/api/s3/profiles/:id", async (_req, res, params, body) => {
1643
+ const existing = configStore.getS3Profile(params["id"]);
1644
+ if (!existing) {
1645
+ sendJson(res, 404, { error: "NOT_FOUND", message: "S3 profile not found", statusCode: 404 });
1646
+ return;
1647
+ }
1648
+ const updates = body;
1649
+ const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt };
1650
+ if (updates.bucket || updates.region || updates.awsProfile !== void 0) {
1651
+ const error = await verifyProfile(updated);
1652
+ updated.verified = !error;
1653
+ }
1654
+ configStore.saveS3Profile(updated);
1655
+ sendJson(res, 200, updated);
1656
+ });
1657
+ server.route("DELETE", "/api/s3/profiles/:id", async (_req, res, params) => {
1658
+ const existing = configStore.getS3Profile(params["id"]);
1659
+ if (!existing) {
1660
+ sendJson(res, 404, { error: "NOT_FOUND", message: "S3 profile not found", statusCode: 404 });
1661
+ return;
1662
+ }
1663
+ const connections = configStore.getS3ConnectionsForProfile(params["id"]);
1664
+ for (const conn of connections) {
1665
+ removeCapabilityFile(agentsDir, conn.targetAgent, "s3", conn.id);
1666
+ configStore.deleteS3Connection(conn.id);
1667
+ }
1668
+ configStore.deleteS3Profile(params["id"]);
1669
+ sendJson(res, 200, { deleted: true });
1670
+ });
1671
+ server.route("GET", "/api/s3/connections", async (_req, res) => {
1672
+ sendJson(res, 200, { connections: configStore.getS3Connections() });
1673
+ });
1674
+ server.route("POST", "/api/s3/connections", async (_req, res, _params, body) => {
1675
+ const input = body;
1676
+ if (!input?.profileId || !input?.targetAgent || !input?.purpose) {
1677
+ sendJson(res, 400, { error: "VALIDATION", message: "profileId, targetAgent, and purpose are required", statusCode: 400 });
1678
+ return;
1679
+ }
1680
+ const profile = configStore.getS3Profile(input.profileId);
1681
+ if (!profile) {
1682
+ sendJson(res, 404, { error: "NOT_FOUND", message: "S3 profile not found", statusCode: 404 });
1683
+ return;
1684
+ }
1685
+ if (!readAgent(agentsDir, input.targetAgent)) {
1686
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Target agent not found", statusCode: 404 });
1687
+ return;
1688
+ }
1689
+ const connection = {
1690
+ id: randomUUID2(),
1691
+ profileId: input.profileId,
1692
+ targetAgent: input.targetAgent,
1693
+ purpose: input.purpose,
1694
+ enabled: true,
1695
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1696
+ };
1697
+ configStore.saveS3Connection(connection);
1698
+ generateCapabilityFile(agentsDir, connection.targetAgent, "s3", connection.id, buildS3CapabilityContent(profile, connection));
1699
+ sendJson(res, 201, connection);
1700
+ });
1701
+ server.route("PUT", "/api/s3/connections/:id", async (_req, res, params, body) => {
1702
+ const existing = configStore.getS3Connections().find((c) => c.id === params["id"]);
1703
+ if (!existing) {
1704
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
1705
+ return;
1706
+ }
1707
+ const updates = body ?? {};
1708
+ const updated = {
1709
+ ...existing,
1710
+ targetAgent: updates.targetAgent ?? existing.targetAgent,
1711
+ purpose: updates.purpose ?? existing.purpose,
1712
+ enabled: updates.enabled ?? existing.enabled
1713
+ };
1714
+ configStore.saveS3Connection(updated);
1715
+ const profile = configStore.getS3Profile(updated.profileId);
1716
+ if (profile) {
1717
+ if (updates.targetAgent && updates.targetAgent !== existing.targetAgent) {
1718
+ removeCapabilityFile(agentsDir, existing.targetAgent, "s3", existing.id);
1719
+ }
1720
+ generateCapabilityFile(agentsDir, updated.targetAgent, "s3", updated.id, buildS3CapabilityContent(profile, updated));
1721
+ }
1722
+ sendJson(res, 200, updated);
1723
+ });
1724
+ server.route("DELETE", "/api/s3/connections/:id", async (_req, res, params) => {
1725
+ const existing = configStore.getS3Connections().find((c) => c.id === params["id"]);
1726
+ if (!existing) {
1727
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
1728
+ return;
1729
+ }
1730
+ configStore.deleteS3Connection(params["id"]);
1731
+ removeCapabilityFile(agentsDir, existing.targetAgent, "s3", existing.id);
1732
+ sendJson(res, 200, { deleted: true });
1733
+ });
1734
+ };
1735
+
1736
+ // ../daemon/src/api/routes/email.ts
1737
+ import { randomUUID as randomUUID3 } from "crypto";
1738
+
1739
+ // ../crypto/src/crypto.ts
1740
+ import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
1741
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1742
+ import { join as join2 } from "path";
1743
+ import { homedir as homedir2 } from "os";
1744
+ var IV_LENGTH = 16;
1745
+ var ALGORITHM = "aes-256-cbc";
1746
+ var keyPath = () => join2(process.env["KINDFLOW_DATA_DIR"] ?? join2(homedir2(), ".kindflow"), ".encryption-key");
1747
+ var getOrCreateKey2 = () => {
1748
+ const path = keyPath();
1749
+ try {
1750
+ return readFileSync2(path, "utf-8").trim();
1751
+ } catch {
1752
+ const key = randomBytes(32).toString("hex");
1753
+ mkdirSync3(join2(path, ".."), { recursive: true });
1754
+ writeFileSync3(path, key, { mode: 384 });
1755
+ return key;
1756
+ }
1757
+ };
1758
+ var encrypt2 = (text, key) => {
1759
+ const iv = randomBytes(IV_LENGTH);
1760
+ const cipher = createCipheriv(ALGORITHM, Buffer.from(key, "hex"), iv);
1761
+ let encrypted = cipher.update(text, "utf8", "hex");
1762
+ encrypted += cipher.final("hex");
1763
+ return `${iv.toString("hex")}:${encrypted}`;
1764
+ };
1765
+ var decrypt2 = (text, key) => {
1766
+ if (!text) return "";
1767
+ if (!text.includes(":")) {
1768
+ console.warn("[crypto] decrypt called on value without iv:ciphertext format \u2014 returning raw value.");
1769
+ return text;
1770
+ }
1771
+ const [ivHex, encrypted] = text.split(":");
1772
+ const decipher = createDecipheriv(ALGORITHM, Buffer.from(key, "hex"), Buffer.from(ivHex, "hex"));
1773
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
1774
+ decrypted += decipher.final("utf8");
1775
+ return decrypted;
1776
+ };
1777
+
1778
+ // ../daemon/src/api/routes/email.ts
1779
+ var escapeHtml = (str) => str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1780
+ var buildEmailCapabilityContent = (account, connection) => [
1781
+ `# Email: ${account.name}`,
1782
+ `Account: ${account.email}`,
1783
+ `Command: /${connection.command}`,
1784
+ `Connected to agent: ${connection.targetAgent}`
1785
+ ].join("\n");
1786
+ var registerEmailRoutes = (server, processManager, configStore, daemonPort, agentsDir) => {
1787
+ let encryptionKey = null;
1788
+ const getKey = () => {
1789
+ if (!encryptionKey) encryptionKey = getOrCreateKey2();
1790
+ return encryptionKey;
1791
+ };
1792
+ const pendingOAuthStates = /* @__PURE__ */ new Map();
1793
+ const OAUTH_STATE_TTL = 10 * 60 * 1e3;
1794
+ setInterval(() => {
1795
+ const now = Date.now();
1796
+ for (const [state, expiry] of pendingOAuthStates) {
1797
+ if (now > expiry) pendingOAuthStates.delete(state);
1798
+ }
1799
+ }, 5 * 60 * 1e3).unref();
1800
+ const syncImapRunner = () => {
1801
+ const proc = processManager.get("email-imap");
1802
+ const oauthCreds = configStore.getOAuthCredentials("gmail");
1803
+ const accounts = configStore.getEmailAccounts();
1804
+ const connections = configStore.getEmailConnections();
1805
+ if (proc && proc.status === "running") {
1806
+ if (!oauthCreds) return;
1807
+ processManager.sendToChild("email-imap", {
1808
+ type: "email:update-accounts",
1809
+ accounts,
1810
+ oauthCredentials: oauthCreds
1811
+ });
1812
+ processManager.sendToChild("email-imap", {
1813
+ type: "email:update-connections",
1814
+ connections
1815
+ });
1816
+ } else if (accounts.length > 0 && connections.length > 0 && oauthCreds) {
1817
+ const key = getKey();
1818
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1819
+ processManager.start("email-imap", "helper", {
1820
+ id: "email-imap",
1821
+ kind: "email-imap",
1822
+ name: "Email IMAP Watcher",
1823
+ config: {
1824
+ accounts: accounts.map((a) => ({
1825
+ ...a,
1826
+ refreshToken: decrypt2(a.refreshToken, key)
1827
+ })),
1828
+ connections,
1829
+ oauthCredentials: {
1830
+ clientId: oauthCreds.clientId,
1831
+ clientSecret: decrypt2(oauthCreds.clientSecret, key)
1832
+ }
1833
+ },
1834
+ autoStart: true,
1835
+ createdAt: now,
1836
+ updatedAt: now
1837
+ });
1838
+ console.log(`[email] Auto-started email-imap runner with ${accounts.length} account(s)`);
1839
+ }
1840
+ };
1841
+ server.route("POST", "/api/email/oauth/credentials", async (_req, res, _params, body) => {
1842
+ const { clientId, clientSecret } = body ?? {};
1843
+ if (!clientId || !clientSecret) {
1844
+ sendJson(res, 400, { error: "VALIDATION", message: "clientId and clientSecret are required", statusCode: 400 });
1845
+ return;
1846
+ }
1847
+ const encrypted = {
1848
+ clientId,
1849
+ clientSecret: encrypt2(clientSecret, getKey())
1850
+ };
1851
+ configStore.saveOAuthCredentials("gmail", encrypted);
1852
+ sendJson(res, 200, { saved: true });
1853
+ });
1854
+ server.route("GET", "/api/email/oauth/credentials", async (_req, res) => {
1855
+ const creds = configStore.getOAuthCredentials("gmail");
1856
+ sendJson(res, 200, { configured: !!creds, clientId: creds?.clientId ?? null });
1857
+ });
1858
+ server.route("GET", "/api/email/oauth/start", async (_req, res) => {
1859
+ const creds = configStore.getOAuthCredentials("gmail");
1860
+ if (!creds) {
1861
+ sendJson(res, 400, { error: "NOT_CONFIGURED", message: "OAuth credentials not configured", statusCode: 400 });
1862
+ return;
1863
+ }
1864
+ const redirectUri = `http://localhost:${daemonPort}/api/email/oauth/callback`;
1865
+ const state = randomUUID3();
1866
+ pendingOAuthStates.set(state, Date.now() + OAUTH_STATE_TTL);
1867
+ const url = buildConsentUrl(creds.clientId, redirectUri, state);
1868
+ sendJson(res, 200, { url, state });
1869
+ });
1870
+ server.route("GET", "/api/email/oauth/callback", async (req, res) => {
1871
+ const url = new URL(req.url ?? "/", `http://localhost:${daemonPort}`);
1872
+ const code = url.searchParams.get("code");
1873
+ const state = url.searchParams.get("state");
1874
+ if (!code) {
1875
+ sendJson(res, 400, { error: "MISSING_CODE", message: "Authorization code not found", statusCode: 400 });
1876
+ return;
1877
+ }
1878
+ if (!state || !pendingOAuthStates.has(state)) {
1879
+ sendJson(res, 400, { error: "INVALID_STATE", message: "Invalid or missing OAuth state parameter", statusCode: 400 });
1880
+ return;
1881
+ }
1882
+ const expiry = pendingOAuthStates.get(state);
1883
+ pendingOAuthStates.delete(state);
1884
+ if (Date.now() > expiry) {
1885
+ sendJson(res, 400, { error: "EXPIRED_STATE", message: "OAuth state has expired, please try again", statusCode: 400 });
1886
+ return;
1887
+ }
1888
+ const creds = configStore.getOAuthCredentials("gmail");
1889
+ if (!creds) {
1890
+ sendJson(res, 400, { error: "NOT_CONFIGURED", message: "OAuth credentials not configured", statusCode: 400 });
1891
+ return;
1892
+ }
1893
+ const clientSecret = decrypt2(creds.clientSecret, getKey());
1894
+ const redirectUri = `http://localhost:${daemonPort}/api/email/oauth/callback`;
1895
+ try {
1896
+ const tokens = await exchangeCodeForTokens(code, creds.clientId, clientSecret, redirectUri);
1897
+ const userInfo = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
1898
+ headers: { Authorization: `Bearer ${tokens.accessToken}` }
1899
+ });
1900
+ const { email } = await userInfo.json();
1901
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1902
+ const account = {
1903
+ id: randomUUID3(),
1904
+ name: email,
1905
+ email,
1906
+ refreshToken: encrypt2(tokens.refreshToken, getKey()),
1907
+ createdAt: now,
1908
+ updatedAt: now
1909
+ };
1910
+ configStore.saveEmailAccount(account);
1911
+ syncImapRunner();
1912
+ const safeEmail = escapeHtml(email);
1913
+ res.writeHead(200, { "Content-Type": "text/html" });
1914
+ res.end(`<html><body><script>window.close();</script><p>Account added: ${safeEmail}. You can close this window.</p></body></html>`);
1915
+ } catch (err) {
1916
+ const message = err instanceof Error ? err.message : "Unknown error";
1917
+ sendJson(res, 500, { error: "OAUTH_EXCHANGE_FAILED", message, statusCode: 500 });
1918
+ }
1919
+ });
1920
+ server.route("GET", "/api/email/accounts", async (_req, res) => {
1921
+ const accounts = configStore.getEmailAccounts().map((a) => ({
1922
+ ...a,
1923
+ refreshToken: "***"
1924
+ }));
1925
+ sendJson(res, 200, { accounts });
1926
+ });
1927
+ server.route("DELETE", "/api/email/accounts/:id", async (_req, res, params) => {
1928
+ const account = configStore.getEmailAccount(params["id"]);
1929
+ if (!account) {
1930
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Account not found", statusCode: 404 });
1931
+ return;
1932
+ }
1933
+ const connections = configStore.getEmailConnectionsForAccount(params["id"]);
1934
+ for (const conn of connections) {
1935
+ removeCapabilityFile(agentsDir, conn.targetAgent, "email", conn.id);
1936
+ }
1937
+ configStore.deleteEmailAccount(params["id"]);
1938
+ syncImapRunner();
1939
+ sendJson(res, 200, { deleted: true });
1940
+ });
1941
+ server.route("POST", "/api/email/accounts/:id/test", async (_req, res, params) => {
1942
+ const account = configStore.getEmailAccount(params["id"]);
1943
+ if (!account) {
1944
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Account not found", statusCode: 404 });
1945
+ return;
1946
+ }
1947
+ const creds = configStore.getOAuthCredentials("gmail");
1948
+ if (!creds) {
1949
+ sendJson(res, 400, { error: "NOT_CONFIGURED", message: "OAuth credentials not configured", statusCode: 400 });
1950
+ return;
1951
+ }
1952
+ try {
1953
+ const refreshToken = decrypt2(account.refreshToken, getKey());
1954
+ const clientSecret = decrypt2(creds.clientSecret, getKey());
1955
+ const { accessToken } = await refreshAccessToken(refreshToken, creds.clientId, clientSecret);
1956
+ const client = new ImapClient();
1957
+ await client.connect();
1958
+ const token = buildXOAuth2Token(account.email, accessToken);
1959
+ await client.authenticate(token);
1960
+ const messageCount = await client.select("INBOX");
1961
+ await client.logout();
1962
+ sendJson(res, 200, { success: true, messageCount });
1963
+ } catch (err) {
1964
+ const message = err instanceof Error ? err.message : "Connection failed";
1965
+ sendJson(res, 200, { success: false, error: message });
1966
+ }
1967
+ });
1968
+ server.route("GET", "/api/email/connections", async (_req, res) => {
1969
+ sendJson(res, 200, { connections: configStore.getEmailConnections() });
1970
+ });
1971
+ server.route("POST", "/api/email/connections", async (_req, res, _params, body) => {
1972
+ const { accountId, targetAgent, command, enabled } = body ?? {};
1973
+ if (!accountId || !targetAgent || !command) {
1974
+ sendJson(res, 400, { error: "VALIDATION", message: "accountId, targetAgent, and command are required", statusCode: 400 });
1975
+ return;
1976
+ }
1977
+ if (!configStore.getEmailAccount(accountId)) {
1978
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Account not found", statusCode: 404 });
1979
+ return;
1980
+ }
1981
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1982
+ const connection = {
1983
+ id: randomUUID3(),
1984
+ accountId,
1985
+ targetAgent,
1986
+ command,
1987
+ enabled: enabled !== false,
1988
+ createdAt: now,
1989
+ updatedAt: now
1990
+ };
1991
+ configStore.saveEmailConnection(connection);
1992
+ const parentAccount = configStore.getEmailAccount(accountId);
1993
+ if (parentAccount) {
1994
+ generateCapabilityFile(agentsDir, connection.targetAgent, "email", connection.id, buildEmailCapabilityContent(parentAccount, connection));
1995
+ }
1996
+ syncImapRunner();
1997
+ sendJson(res, 201, connection);
1998
+ });
1999
+ server.route("PUT", "/api/email/connections/:id", async (_req, res, params, body) => {
2000
+ const existing = configStore.getEmailConnection(params["id"]);
2001
+ if (!existing) {
2002
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2003
+ return;
2004
+ }
2005
+ const updates = body ?? {};
2006
+ const updated = {
2007
+ ...existing,
2008
+ targetAgent: updates.targetAgent ?? existing.targetAgent,
2009
+ command: updates.command ?? existing.command,
2010
+ enabled: updates.enabled ?? existing.enabled,
2011
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2012
+ };
2013
+ configStore.saveEmailConnection(updated);
2014
+ const parentAccount = configStore.getEmailAccount(updated.accountId);
2015
+ if (parentAccount) {
2016
+ if (updates.targetAgent && updates.targetAgent !== existing.targetAgent) {
2017
+ removeCapabilityFile(agentsDir, existing.targetAgent, "email", existing.id);
2018
+ }
2019
+ generateCapabilityFile(agentsDir, updated.targetAgent, "email", updated.id, buildEmailCapabilityContent(parentAccount, updated));
2020
+ }
2021
+ syncImapRunner();
2022
+ sendJson(res, 200, updated);
2023
+ });
2024
+ server.route("DELETE", "/api/email/connections/:id", async (_req, res, params) => {
2025
+ const existing = configStore.getEmailConnection(params["id"]);
2026
+ if (!existing) {
2027
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2028
+ return;
2029
+ }
2030
+ configStore.deleteEmailConnection(params["id"]);
2031
+ removeCapabilityFile(agentsDir, existing.targetAgent, "email", existing.id);
2032
+ syncImapRunner();
2033
+ sendJson(res, 200, { deleted: true });
2034
+ });
2035
+ };
2036
+
2037
+ // ../daemon/src/api/routes/database.ts
2038
+ import { randomUUID as randomUUID4 } from "crypto";
2039
+ var redactPassword = (profile) => ({
2040
+ ...profile,
2041
+ password: profile.password ? "***" : null
2042
+ });
2043
+ var verifyProfile2 = async (profile, key) => {
2044
+ try {
2045
+ const client = await createDatabaseClient({
2046
+ type: profile.type,
2047
+ host: profile.host ?? void 0,
2048
+ port: profile.port ?? void 0,
2049
+ database: profile.database,
2050
+ username: profile.username ?? void 0,
2051
+ password: profile.password ? decrypt2(profile.password, key) : void 0,
2052
+ sslMode: profile.sslMode ?? void 0
2053
+ });
2054
+ const error = await client.verify();
2055
+ await client.close();
2056
+ return error;
2057
+ } catch (err) {
2058
+ return err instanceof Error ? err.message : String(err);
2059
+ }
2060
+ };
2061
+ var buildDatabaseCapabilityContent = (profile, connection) => {
2062
+ const lines = [
2063
+ `# Database: ${profile.name}`,
2064
+ `Type: ${profile.type}`
2065
+ ];
2066
+ if (profile.host) lines.push(`Host: ${profile.host}${profile.port ? `:${profile.port}` : ""}`);
2067
+ lines.push(`Database: ${profile.database}`);
2068
+ lines.push("", "## Usage", connection.purpose);
2069
+ lines.push("", "## Code", "```ts");
2070
+ lines.push("import { createDatabaseClient } from 'kindflow/database';");
2071
+ lines.push("const db = await createDatabaseClient({");
2072
+ lines.push(` type: '${profile.type}',`);
2073
+ if (profile.host) lines.push(` host: '${profile.host}',`);
2074
+ if (profile.port) lines.push(` port: ${profile.port},`);
2075
+ lines.push(` database: '${profile.database}',`);
2076
+ if (profile.username) lines.push(` username: '...',`);
2077
+ if (profile.password) lines.push(` password: '...',`);
2078
+ lines.push("});");
2079
+ lines.push("```");
2080
+ return lines.join("\n");
2081
+ };
2082
+ var registerDatabaseRoutes = (server, configStore, agentsDir) => {
2083
+ let encryptionKey = null;
2084
+ const getKey = () => {
2085
+ if (!encryptionKey) encryptionKey = getOrCreateKey2();
2086
+ return encryptionKey;
2087
+ };
2088
+ server.route("GET", "/api/database/profiles", async (_req, res) => {
2089
+ sendJson(res, 200, { profiles: configStore.getDatabaseProfiles().map(redactPassword) });
2090
+ });
2091
+ server.route("GET", "/api/database/profiles/:id", async (_req, res, params) => {
2092
+ const profile = configStore.getDatabaseProfile(params["id"]);
2093
+ if (!profile) {
2094
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Database profile not found", statusCode: 404 });
2095
+ return;
2096
+ }
2097
+ sendJson(res, 200, redactPassword(profile));
2098
+ });
2099
+ server.route("POST", "/api/database/profiles", async (_req, res, _params, body) => {
2100
+ const input = body;
2101
+ if (!input?.name || !input?.type || !input?.database) {
2102
+ sendJson(res, 400, { error: "BAD_REQUEST", message: "name, type, and database are required", statusCode: 400 });
2103
+ return;
2104
+ }
2105
+ const profile = {
2106
+ id: input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || randomUUID4().slice(0, 8),
2107
+ name: input.name,
2108
+ type: input.type,
2109
+ host: input.host ?? null,
2110
+ port: input.port ?? null,
2111
+ database: input.database,
2112
+ username: input.username ?? null,
2113
+ password: input.password ? encrypt2(input.password, getKey()) : null,
2114
+ sslMode: input.sslMode ?? null,
2115
+ verified: false,
2116
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2117
+ };
2118
+ const error = await verifyProfile2(profile, getKey());
2119
+ if (error) {
2120
+ sendJson(res, 400, { error: "VERIFICATION_FAILED", message: error, statusCode: 400 });
2121
+ return;
2122
+ }
2123
+ profile.verified = true;
2124
+ configStore.saveDatabaseProfile(profile);
2125
+ sendJson(res, 201, redactPassword(profile));
2126
+ });
2127
+ server.route("POST", "/api/database/profiles/:id/verify", async (_req, res, params) => {
2128
+ const profile = configStore.getDatabaseProfile(params["id"]);
2129
+ if (!profile) {
2130
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Database profile not found", statusCode: 404 });
2131
+ return;
2132
+ }
2133
+ const error = await verifyProfile2(profile, getKey());
2134
+ profile.verified = !error;
2135
+ configStore.saveDatabaseProfile(profile);
2136
+ sendJson(res, 200, { verified: !error, error: error ?? void 0 });
2137
+ });
2138
+ server.route("PUT", "/api/database/profiles/:id", async (_req, res, params, body) => {
2139
+ const existing = configStore.getDatabaseProfile(params["id"]);
2140
+ if (!existing) {
2141
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Database profile not found", statusCode: 404 });
2142
+ return;
2143
+ }
2144
+ const updates = body;
2145
+ const updatedPassword = updates.password !== void 0 ? updates.password ? encrypt2(updates.password, getKey()) : null : existing.password;
2146
+ const updated = {
2147
+ ...existing,
2148
+ ...updates,
2149
+ id: existing.id,
2150
+ createdAt: existing.createdAt,
2151
+ password: updatedPassword
2152
+ };
2153
+ if (updates.host !== void 0 || updates.port !== void 0 || updates.database !== void 0 || updates.username !== void 0 || updates.password !== void 0 || updates.sslMode !== void 0 || updates.type !== void 0) {
2154
+ const error = await verifyProfile2(updated, getKey());
2155
+ updated.verified = !error;
2156
+ }
2157
+ configStore.saveDatabaseProfile(updated);
2158
+ sendJson(res, 200, redactPassword(updated));
2159
+ });
2160
+ server.route("DELETE", "/api/database/profiles/:id", async (_req, res, params) => {
2161
+ const existing = configStore.getDatabaseProfile(params["id"]);
2162
+ if (!existing) {
2163
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Database profile not found", statusCode: 404 });
2164
+ return;
2165
+ }
2166
+ const connections = configStore.getDatabaseConnectionsForProfile(params["id"]);
2167
+ for (const conn of connections) {
2168
+ removeCapabilityFile(agentsDir, conn.targetAgent, "database", conn.id);
2169
+ configStore.deleteDatabaseConnection(conn.id);
2170
+ }
2171
+ configStore.deleteDatabaseProfile(params["id"]);
2172
+ sendJson(res, 200, { deleted: true });
2173
+ });
2174
+ server.route("GET", "/api/database/connections", async (_req, res) => {
2175
+ sendJson(res, 200, { connections: configStore.getDatabaseConnections() });
2176
+ });
2177
+ server.route("POST", "/api/database/connections", async (_req, res, _params, body) => {
2178
+ const input = body;
2179
+ if (!input?.profileId || !input?.targetAgent || !input?.purpose) {
2180
+ sendJson(res, 400, { error: "VALIDATION", message: "profileId, targetAgent, and purpose are required", statusCode: 400 });
2181
+ return;
2182
+ }
2183
+ const profile = configStore.getDatabaseProfile(input.profileId);
2184
+ if (!profile) {
2185
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Database profile not found", statusCode: 404 });
2186
+ return;
2187
+ }
2188
+ if (!readAgent(agentsDir, input.targetAgent)) {
2189
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Target agent not found", statusCode: 404 });
2190
+ return;
2191
+ }
2192
+ const connection = {
2193
+ id: randomUUID4(),
2194
+ profileId: input.profileId,
2195
+ targetAgent: input.targetAgent,
2196
+ purpose: input.purpose,
2197
+ enabled: true,
2198
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2199
+ };
2200
+ configStore.saveDatabaseConnection(connection);
2201
+ generateCapabilityFile(agentsDir, connection.targetAgent, "database", connection.id, buildDatabaseCapabilityContent(profile, connection));
2202
+ sendJson(res, 201, connection);
2203
+ });
2204
+ server.route("PUT", "/api/database/connections/:id", async (_req, res, params, body) => {
2205
+ const existing = configStore.getDatabaseConnections().find((c) => c.id === params["id"]);
2206
+ if (!existing) {
2207
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2208
+ return;
2209
+ }
2210
+ const updates = body ?? {};
2211
+ const updated = {
2212
+ ...existing,
2213
+ targetAgent: updates.targetAgent ?? existing.targetAgent,
2214
+ purpose: updates.purpose ?? existing.purpose,
2215
+ enabled: updates.enabled ?? existing.enabled
2216
+ };
2217
+ configStore.saveDatabaseConnection(updated);
2218
+ const profile = configStore.getDatabaseProfile(updated.profileId);
2219
+ if (profile) {
2220
+ if (updates.targetAgent && updates.targetAgent !== existing.targetAgent) {
2221
+ removeCapabilityFile(agentsDir, existing.targetAgent, "database", existing.id);
2222
+ }
2223
+ generateCapabilityFile(agentsDir, updated.targetAgent, "database", updated.id, buildDatabaseCapabilityContent(profile, updated));
2224
+ }
2225
+ sendJson(res, 200, updated);
2226
+ });
2227
+ server.route("DELETE", "/api/database/connections/:id", async (_req, res, params) => {
2228
+ const existing = configStore.getDatabaseConnections().find((c) => c.id === params["id"]);
2229
+ if (!existing) {
2230
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2231
+ return;
2232
+ }
2233
+ configStore.deleteDatabaseConnection(params["id"]);
2234
+ removeCapabilityFile(agentsDir, existing.targetAgent, "database", existing.id);
2235
+ sendJson(res, 200, { deleted: true });
2236
+ });
2237
+ };
2238
+
2239
+ // ../daemon/src/api/routes/http.ts
2240
+ import { randomUUID as randomUUID5, timingSafeEqual } from "crypto";
2241
+ var PATH_SLUG_REGEX = /^[a-z0-9][a-z0-9-]*$/;
2242
+ var extractBearerToken = (req) => {
2243
+ const header = req.headers["authorization"];
2244
+ if (!header || !header.startsWith("Bearer ")) return null;
2245
+ return header.slice(7);
2246
+ };
2247
+ var safeEqual = (a, b) => {
2248
+ const bufA = Buffer.from(a);
2249
+ const bufB = Buffer.from(b);
2250
+ if (bufA.length !== bufB.length) return false;
2251
+ return timingSafeEqual(bufA, bufB);
2252
+ };
2253
+ var buildHttpCapabilityContent = (endpoint, connection) => [
2254
+ `# HTTP: ${endpoint.name}`,
2255
+ `Path: /api/trigger/${endpoint.path}`,
2256
+ connection.command ? `Command: /${connection.command}` : "Command: (free-form text from request body)",
2257
+ `Connected to agent: ${connection.targetAgent}`
2258
+ ].join("\n");
2259
+ var registerHttpRoutes = (server, processManager, ipcHub, configStore, agentsDir) => {
2260
+ let encryptionKey = null;
2261
+ const getKey = () => {
2262
+ if (!encryptionKey) encryptionKey = getOrCreateKey2();
2263
+ return encryptionKey;
2264
+ };
2265
+ const redactApiKey = (endpoint) => ({
2266
+ ...endpoint,
2267
+ apiKey: endpoint.apiKey ? "***" : null
2268
+ });
2269
+ server.route("POST", "/api/trigger/:path", async (req, res, params, body) => {
2270
+ const pathSlug = params["path"];
2271
+ const endpoint = configStore.getHttpEndpointByPath(pathSlug);
2272
+ if (!endpoint) {
2273
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Endpoint not found", statusCode: 404 });
2274
+ return;
2275
+ }
2276
+ if (!endpoint.enabled) {
2277
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Endpoint is disabled", statusCode: 404 });
2278
+ return;
2279
+ }
2280
+ if (endpoint.apiKey) {
2281
+ const token = extractBearerToken(req);
2282
+ const decryptedKey = decrypt2(endpoint.apiKey, getKey());
2283
+ if (!token || !safeEqual(token, decryptedKey)) {
2284
+ sendJson(res, 401, { error: "UNAUTHORIZED", message: "Invalid or missing API key", statusCode: 401 });
2285
+ return;
2286
+ }
2287
+ }
2288
+ const connections = configStore.getHttpConnectionsForEndpoint(endpoint.id).filter((c) => c.enabled);
2289
+ if (connections.length === 0) {
2290
+ sendJson(res, 200, { queued: false, reason: "No enabled connections for this endpoint" });
2291
+ return;
2292
+ }
2293
+ let queued = 0;
2294
+ for (const connection of connections) {
2295
+ const proc = processManager.get(connection.targetAgent);
2296
+ if (!proc || proc.status !== "running") continue;
2297
+ let promptText;
2298
+ if (connection.command) {
2299
+ promptText = `/${connection.command}`;
2300
+ } else {
2301
+ const { text } = body ?? {};
2302
+ if (!text) continue;
2303
+ promptText = text;
2304
+ }
2305
+ const message = createMessage("http-trigger", connection.targetAgent, "prompt", {
2306
+ text: promptText,
2307
+ metadata: {
2308
+ source: "http",
2309
+ endpointId: endpoint.id,
2310
+ endpointName: endpoint.name,
2311
+ endpointPath: endpoint.path,
2312
+ connectionId: connection.id
2313
+ }
2314
+ });
2315
+ ipcHub.sendTo(connection.targetAgent, message);
2316
+ queued++;
2317
+ }
2318
+ if (queued === 0) {
2319
+ sendJson(res, 200, { queued: false, count: 0, reason: "No agents were available to receive the message" });
2320
+ return;
2321
+ }
2322
+ sendJson(res, 202, { queued: true, count: queued });
2323
+ });
2324
+ server.route("GET", "/api/http/endpoints", async (_req, res) => {
2325
+ const endpoints = configStore.getHttpEndpoints().map(redactApiKey);
2326
+ sendJson(res, 200, { endpoints });
2327
+ });
2328
+ server.route("POST", "/api/http/endpoints", async (_req, res, _params, body) => {
2329
+ const input = body;
2330
+ if (!input?.name || !input?.path) {
2331
+ sendJson(res, 400, { error: "VALIDATION", message: "name and path are required", statusCode: 400 });
2332
+ return;
2333
+ }
2334
+ if (!PATH_SLUG_REGEX.test(input.path)) {
2335
+ sendJson(res, 400, { error: "BAD_REQUEST", message: "path must contain only lowercase letters, numbers, and hyphens", statusCode: 400 });
2336
+ return;
2337
+ }
2338
+ const existing = configStore.getHttpEndpointByPath(input.path);
2339
+ if (existing) {
2340
+ sendJson(res, 409, { error: "CONFLICT", message: `An endpoint with path "${input.path}" already exists`, statusCode: 409 });
2341
+ return;
2342
+ }
2343
+ const rawApiKey = input.generateApiKey ? randomUUID5() : null;
2344
+ const endpoint = {
2345
+ id: randomUUID5(),
2346
+ name: input.name,
2347
+ path: input.path,
2348
+ apiKey: rawApiKey ? encrypt2(rawApiKey, getKey()) : null,
2349
+ enabled: true,
2350
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2351
+ };
2352
+ configStore.saveHttpEndpoint(endpoint);
2353
+ sendJson(res, 201, redactApiKey(endpoint));
2354
+ });
2355
+ server.route("GET", "/api/http/endpoints/:id", async (_req, res, params) => {
2356
+ const endpoint = configStore.getHttpEndpoint(params["id"]);
2357
+ if (!endpoint) {
2358
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Endpoint not found", statusCode: 404 });
2359
+ return;
2360
+ }
2361
+ sendJson(res, 200, redactApiKey(endpoint));
2362
+ });
2363
+ server.route("PUT", "/api/http/endpoints/:id", async (_req, res, params, body) => {
2364
+ const existing = configStore.getHttpEndpoint(params["id"]);
2365
+ if (!existing) {
2366
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Endpoint not found", statusCode: 404 });
2367
+ return;
2368
+ }
2369
+ const updates = body ?? {};
2370
+ if (updates.path !== void 0 && !PATH_SLUG_REGEX.test(updates.path)) {
2371
+ sendJson(res, 400, { error: "BAD_REQUEST", message: "path must contain only lowercase letters, numbers, and hyphens", statusCode: 400 });
2372
+ return;
2373
+ }
2374
+ if (updates.path && updates.path !== existing.path) {
2375
+ const dup = configStore.getHttpEndpointByPath(updates.path);
2376
+ if (dup) {
2377
+ sendJson(res, 409, { error: "CONFLICT", message: `An endpoint with path "${updates.path}" already exists`, statusCode: 409 });
2378
+ return;
2379
+ }
2380
+ }
2381
+ const rawApiKey = updates.regenerateApiKey ? randomUUID5() : null;
2382
+ const updated = {
2383
+ ...existing,
2384
+ name: updates.name ?? existing.name,
2385
+ path: updates.path ?? existing.path,
2386
+ enabled: updates.enabled ?? existing.enabled,
2387
+ apiKey: rawApiKey ? encrypt2(rawApiKey, getKey()) : existing.apiKey
2388
+ };
2389
+ configStore.saveHttpEndpoint(updated);
2390
+ sendJson(res, 200, redactApiKey(updated));
2391
+ });
2392
+ server.route("DELETE", "/api/http/endpoints/:id", async (_req, res, params) => {
2393
+ const endpoint = configStore.getHttpEndpoint(params["id"]);
2394
+ if (!endpoint) {
2395
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Endpoint not found", statusCode: 404 });
2396
+ return;
2397
+ }
2398
+ const connections = configStore.getHttpConnectionsForEndpoint(params["id"]);
2399
+ for (const conn of connections) {
2400
+ removeCapabilityFile(agentsDir, conn.targetAgent, "http", conn.id);
2401
+ }
2402
+ configStore.deleteHttpEndpoint(params["id"]);
2403
+ sendJson(res, 200, { deleted: true });
2404
+ });
2405
+ server.route("GET", "/api/http/connections", async (_req, res) => {
2406
+ sendJson(res, 200, { connections: configStore.getHttpConnections() });
2407
+ });
2408
+ server.route("POST", "/api/http/connections", async (_req, res, _params, body) => {
2409
+ const input = body;
2410
+ if (!input?.endpointId || !input?.targetAgent) {
2411
+ sendJson(res, 400, { error: "VALIDATION", message: "endpointId and targetAgent are required", statusCode: 400 });
2412
+ return;
2413
+ }
2414
+ if (!configStore.getHttpEndpoint(input.endpointId)) {
2415
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Endpoint not found", statusCode: 404 });
2416
+ return;
2417
+ }
2418
+ const connection = {
2419
+ id: randomUUID5(),
2420
+ endpointId: input.endpointId,
2421
+ targetAgent: input.targetAgent,
2422
+ command: input.command ?? null,
2423
+ enabled: true,
2424
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2425
+ };
2426
+ configStore.saveHttpConnection(connection);
2427
+ const parentEndpoint = configStore.getHttpEndpoint(input.endpointId);
2428
+ if (parentEndpoint) {
2429
+ generateCapabilityFile(agentsDir, connection.targetAgent, "http", connection.id, buildHttpCapabilityContent(parentEndpoint, connection));
2430
+ }
2431
+ sendJson(res, 201, connection);
2432
+ });
2433
+ server.route("PUT", "/api/http/connections/:id", async (_req, res, params, body) => {
2434
+ const existing = configStore.getHttpConnections().find((c) => c.id === params["id"]);
2435
+ if (!existing) {
2436
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2437
+ return;
2438
+ }
2439
+ const updates = body ?? {};
2440
+ const updated = {
2441
+ ...existing,
2442
+ targetAgent: updates.targetAgent ?? existing.targetAgent,
2443
+ command: updates.command !== void 0 ? updates.command : existing.command,
2444
+ enabled: updates.enabled ?? existing.enabled
2445
+ };
2446
+ configStore.saveHttpConnection(updated);
2447
+ const parentEndpoint = configStore.getHttpEndpoint(updated.endpointId);
2448
+ if (parentEndpoint) {
2449
+ if (updates.targetAgent && updates.targetAgent !== existing.targetAgent) {
2450
+ removeCapabilityFile(agentsDir, existing.targetAgent, "http", existing.id);
2451
+ }
2452
+ generateCapabilityFile(agentsDir, updated.targetAgent, "http", updated.id, buildHttpCapabilityContent(parentEndpoint, updated));
2453
+ }
2454
+ sendJson(res, 200, updated);
2455
+ });
2456
+ server.route("DELETE", "/api/http/connections/:id", async (_req, res, params) => {
2457
+ const existing = configStore.getHttpConnections().find((c) => c.id === params["id"]);
2458
+ if (!existing) {
2459
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2460
+ return;
2461
+ }
2462
+ configStore.deleteHttpConnection(params["id"]);
2463
+ removeCapabilityFile(agentsDir, existing.targetAgent, "http", existing.id);
2464
+ sendJson(res, 200, { deleted: true });
2465
+ });
2466
+ };
2467
+
2468
+ // ../daemon/src/api/routes/telegram.ts
2469
+ import { randomUUID as randomUUID6 } from "crypto";
2470
+ var buildTelegramCapabilityContent = (bot, connection) => [
2471
+ `# Telegram: ${bot.name}`,
2472
+ `Bot: @${bot.botUsername}`,
2473
+ `Chat ID: ${connection.chatId}`,
2474
+ `Connected to agent: ${connection.targetAgent}`
2475
+ ].join("\n");
2476
+ var registerTelegramRoutes = (server, processManager, configStore, agentsDir) => {
2477
+ let encryptionKey = null;
2478
+ const getKey = () => {
2479
+ if (!encryptionKey) encryptionKey = getOrCreateKey2();
2480
+ return encryptionKey;
2481
+ };
2482
+ const syncTelegramRunner = () => {
2483
+ const proc = processManager.get("telegram");
2484
+ const bots = configStore.getTelegramBots();
2485
+ const connections = configStore.getTelegramConnections();
2486
+ const decryptedBots = bots.map((b) => ({
2487
+ ...b,
2488
+ token: decrypt2(b.token, getKey())
2489
+ }));
2490
+ if (proc && proc.status === "running") {
2491
+ processManager.sendToChild("telegram", { type: "telegram:update-bots", bots: decryptedBots });
2492
+ processManager.sendToChild("telegram", { type: "telegram:update-connections", connections });
2493
+ } else if (bots.length > 0 && connections.length > 0) {
2494
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2495
+ processManager.start("telegram", "helper", {
2496
+ id: "telegram",
2497
+ kind: "telegram",
2498
+ name: "Telegram Runner",
2499
+ config: { bots: decryptedBots, connections },
2500
+ autoStart: true,
2501
+ createdAt: now,
2502
+ updatedAt: now
2503
+ });
2504
+ console.log(`[telegram] Auto-started telegram-runner with ${bots.length} bot(s)`);
2505
+ }
2506
+ };
2507
+ server.route("GET", "/api/telegram/bots", async (_req, res) => {
2508
+ const bots = configStore.getTelegramBots().map((b) => ({
2509
+ ...b,
2510
+ token: "***"
2511
+ }));
2512
+ sendJson(res, 200, { bots });
2513
+ });
2514
+ server.route("POST", "/api/telegram/bots", async (_req, res, _params, body) => {
2515
+ const input = body;
2516
+ if (!input?.name || !input?.token) {
2517
+ sendJson(res, 400, { error: "VALIDATION", message: "name and token are required", statusCode: 400 });
2518
+ return;
2519
+ }
2520
+ const client = createTelegramClient({ token: input.token });
2521
+ const result = await client.verify();
2522
+ if (!result.ok) {
2523
+ sendJson(res, 400, { error: "VERIFICATION_FAILED", message: `Token verification failed: ${result.error}`, statusCode: 400 });
2524
+ return;
2525
+ }
2526
+ const bot = {
2527
+ id: randomUUID6(),
2528
+ name: input.name,
2529
+ token: encrypt2(input.token, getKey()),
2530
+ botUsername: result.username,
2531
+ polling: true,
2532
+ webhookUrl: null,
2533
+ verified: true,
2534
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2535
+ };
2536
+ configStore.saveTelegramBot(bot);
2537
+ syncTelegramRunner();
2538
+ sendJson(res, 201, { ...bot, token: "***" });
2539
+ });
2540
+ server.route("GET", "/api/telegram/bots/:id", async (_req, res, params) => {
2541
+ const bot = configStore.getTelegramBot(params["id"]);
2542
+ if (!bot) {
2543
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Bot not found", statusCode: 404 });
2544
+ return;
2545
+ }
2546
+ sendJson(res, 200, { ...bot, token: "***" });
2547
+ });
2548
+ server.route("PUT", "/api/telegram/bots/:id", async (_req, res, params, body) => {
2549
+ const existing = configStore.getTelegramBot(params["id"]);
2550
+ if (!existing) {
2551
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Bot not found", statusCode: 404 });
2552
+ return;
2553
+ }
2554
+ const updates = body ?? {};
2555
+ const updated = {
2556
+ ...existing,
2557
+ name: updates.name ?? existing.name,
2558
+ polling: updates.polling ?? existing.polling,
2559
+ webhookUrl: updates.webhookUrl !== void 0 ? updates.webhookUrl : existing.webhookUrl
2560
+ };
2561
+ configStore.saveTelegramBot(updated);
2562
+ syncTelegramRunner();
2563
+ sendJson(res, 200, { ...updated, token: "***" });
2564
+ });
2565
+ server.route("DELETE", "/api/telegram/bots/:id", async (_req, res, params) => {
2566
+ const bot = configStore.getTelegramBot(params["id"]);
2567
+ if (!bot) {
2568
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Bot not found", statusCode: 404 });
2569
+ return;
2570
+ }
2571
+ const connections = configStore.getTelegramConnectionsForBot(params["id"]);
2572
+ for (const conn of connections) {
2573
+ removeCapabilityFile(agentsDir, conn.targetAgent, "telegram", conn.id);
2574
+ }
2575
+ configStore.deleteTelegramBot(params["id"]);
2576
+ syncTelegramRunner();
2577
+ sendJson(res, 200, { deleted: true });
2578
+ });
2579
+ server.route("POST", "/api/telegram/bots/:id/verify", async (_req, res, params) => {
2580
+ const bot = configStore.getTelegramBot(params["id"]);
2581
+ if (!bot) {
2582
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Bot not found", statusCode: 404 });
2583
+ return;
2584
+ }
2585
+ const token = decrypt2(bot.token, getKey());
2586
+ const client = createTelegramClient({ token });
2587
+ const result = await client.verify();
2588
+ if (!result.ok) {
2589
+ bot.verified = false;
2590
+ configStore.saveTelegramBot(bot);
2591
+ sendJson(res, 200, { verified: false, error: result.error });
2592
+ return;
2593
+ }
2594
+ bot.verified = true;
2595
+ bot.botUsername = result.username;
2596
+ configStore.saveTelegramBot(bot);
2597
+ syncTelegramRunner();
2598
+ sendJson(res, 200, { verified: true, username: result.username });
2599
+ });
2600
+ server.route("GET", "/api/telegram/connections", async (_req, res) => {
2601
+ sendJson(res, 200, { connections: configStore.getTelegramConnections() });
2602
+ });
2603
+ server.route("POST", "/api/telegram/connections", async (_req, res, _params, body) => {
2604
+ const input = body;
2605
+ if (!input?.botId || !input?.targetAgent) {
2606
+ sendJson(res, 400, { error: "VALIDATION", message: "botId and targetAgent are required", statusCode: 400 });
2607
+ return;
2608
+ }
2609
+ if (!configStore.getTelegramBot(input.botId)) {
2610
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Bot not found", statusCode: 404 });
2611
+ return;
2612
+ }
2613
+ const connection = {
2614
+ id: randomUUID6(),
2615
+ botId: input.botId,
2616
+ targetAgent: input.targetAgent,
2617
+ chatId: input.chatId ?? "*",
2618
+ enabled: input.enabled !== false,
2619
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2620
+ };
2621
+ configStore.saveTelegramConnection(connection);
2622
+ const parentBot = configStore.getTelegramBot(input.botId);
2623
+ if (parentBot) {
2624
+ generateCapabilityFile(agentsDir, connection.targetAgent, "telegram", connection.id, buildTelegramCapabilityContent(parentBot, connection));
2625
+ }
2626
+ syncTelegramRunner();
2627
+ sendJson(res, 201, connection);
2628
+ });
2629
+ server.route("PUT", "/api/telegram/connections/:id", async (_req, res, params, body) => {
2630
+ const existing = configStore.getTelegramConnections().find((c) => c.id === params["id"]);
2631
+ if (!existing) {
2632
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2633
+ return;
2634
+ }
2635
+ const updates = body ?? {};
2636
+ const updated = {
2637
+ ...existing,
2638
+ targetAgent: updates.targetAgent ?? existing.targetAgent,
2639
+ chatId: updates.chatId ?? existing.chatId,
2640
+ enabled: updates.enabled ?? existing.enabled
2641
+ };
2642
+ configStore.saveTelegramConnection(updated);
2643
+ const parentBot = configStore.getTelegramBot(updated.botId);
2644
+ if (parentBot) {
2645
+ if (updates.targetAgent && updates.targetAgent !== existing.targetAgent) {
2646
+ removeCapabilityFile(agentsDir, existing.targetAgent, "telegram", existing.id);
2647
+ }
2648
+ generateCapabilityFile(agentsDir, updated.targetAgent, "telegram", updated.id, buildTelegramCapabilityContent(parentBot, updated));
2649
+ }
2650
+ syncTelegramRunner();
2651
+ sendJson(res, 200, updated);
2652
+ });
2653
+ server.route("DELETE", "/api/telegram/connections/:id", async (_req, res, params) => {
2654
+ const existing = configStore.getTelegramConnections().find((c) => c.id === params["id"]);
2655
+ if (!existing) {
2656
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2657
+ return;
2658
+ }
2659
+ configStore.deleteTelegramConnection(params["id"]);
2660
+ removeCapabilityFile(agentsDir, existing.targetAgent, "telegram", existing.id);
2661
+ syncTelegramRunner();
2662
+ sendJson(res, 200, { deleted: true });
2663
+ });
2664
+ };
2665
+
2666
+ // ../daemon/src/api/routes/cron.ts
2667
+ import { randomUUID as randomUUID7 } from "crypto";
2668
+ var buildCronRunnerConfig = (configStore) => {
2669
+ const jobs = configStore.getCronJobs().filter((j) => j.enabled);
2670
+ const configs = [];
2671
+ for (const job of jobs) {
2672
+ const connections = configStore.getCronConnectionsForJob(job.id).filter((c) => c.enabled);
2673
+ for (const conn of connections) {
2674
+ configs.push({
2675
+ id: `${job.id}-${conn.id}`,
2676
+ name: job.name,
2677
+ schedule: job.schedule,
2678
+ command: conn.command ?? job.name,
2679
+ targetAgent: conn.targetAgent,
2680
+ enabled: true,
2681
+ createdAt: job.createdAt,
2682
+ updatedAt: job.createdAt
2683
+ });
2684
+ }
2685
+ }
2686
+ return configs;
2687
+ };
2688
+ var syncCronRunner = (processManager, configStore) => {
2689
+ const allJobs = buildCronRunnerConfig(configStore);
2690
+ const cronProcess = processManager.get("cron");
2691
+ if (cronProcess && cronProcess.status === "running") {
2692
+ processManager.sendToChild("cron", { type: "cron:update-jobs", jobs: allJobs });
2693
+ } else if (allJobs.length > 0) {
2694
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2695
+ processManager.start("cron", "helper", {
2696
+ id: "cron",
2697
+ kind: "cron",
2698
+ name: "Cron Runner",
2699
+ config: { cronJobs: allJobs },
2700
+ autoStart: true,
2701
+ createdAt: now,
2702
+ updatedAt: now
2703
+ });
2704
+ console.log(`[cron] Auto-started cron-runner with ${allJobs.length} job(s)`);
2705
+ }
2706
+ console.log(`[cron] Synced ${allJobs.length} cron job config(s)`);
2707
+ };
2708
+ var buildCronCapabilityContent = (job, connection) => [
2709
+ `# Cron: ${job.name}`,
2710
+ `Schedule: ${job.schedule}`,
2711
+ `Command: /${connection.command ?? job.name}`,
2712
+ `Connected to agent: ${connection.targetAgent}`
2713
+ ].join("\n");
2714
+ var registerCronRoutes = (server, processManager, configStore, agentsDir) => {
2715
+ server.route("GET", "/api/cron/jobs", async (_req, res) => {
2716
+ sendJson(res, 200, { cronJobs: configStore.getCronJobs() });
2717
+ });
2718
+ server.route("POST", "/api/cron/jobs", async (_req, res, _params, body) => {
2719
+ const input = body;
2720
+ if (!input?.name || !input?.schedule) {
2721
+ sendJson(res, 400, { error: "VALIDATION", message: "name and schedule are required", statusCode: 400 });
2722
+ return;
2723
+ }
2724
+ const job = {
2725
+ id: randomUUID7(),
2726
+ name: input.name,
2727
+ schedule: input.schedule,
2728
+ enabled: true,
2729
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2730
+ };
2731
+ configStore.saveCronJob(job);
2732
+ syncCronRunner(processManager, configStore);
2733
+ sendJson(res, 201, job);
2734
+ });
2735
+ server.route("GET", "/api/cron/jobs/:id", async (_req, res, params) => {
2736
+ const job = configStore.getCronJob(params["id"]);
2737
+ if (!job) {
2738
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Cron job not found", statusCode: 404 });
2739
+ return;
2740
+ }
2741
+ sendJson(res, 200, job);
2742
+ });
2743
+ server.route("PUT", "/api/cron/jobs/:id", async (_req, res, params, body) => {
2744
+ const existing = configStore.getCronJob(params["id"]);
2745
+ if (!existing) {
2746
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Cron job not found", statusCode: 404 });
2747
+ return;
2748
+ }
2749
+ const updates = body ?? {};
2750
+ const updated = {
2751
+ ...existing,
2752
+ name: updates.name ?? existing.name,
2753
+ schedule: updates.schedule ?? existing.schedule,
2754
+ enabled: updates.enabled ?? existing.enabled
2755
+ };
2756
+ configStore.saveCronJob(updated);
2757
+ syncCronRunner(processManager, configStore);
2758
+ sendJson(res, 200, updated);
2759
+ });
2760
+ server.route("DELETE", "/api/cron/jobs/:id", async (_req, res, params) => {
2761
+ const job = configStore.getCronJob(params["id"]);
2762
+ if (!job) {
2763
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Cron job not found", statusCode: 404 });
2764
+ return;
2765
+ }
2766
+ const connections = configStore.getCronConnectionsForJob(params["id"]);
2767
+ for (const conn of connections) {
2768
+ removeCapabilityFile(agentsDir, conn.targetAgent, "cron", conn.id);
2769
+ }
2770
+ configStore.deleteCronJob(params["id"]);
2771
+ syncCronRunner(processManager, configStore);
2772
+ sendJson(res, 200, { deleted: true });
2773
+ });
2774
+ server.route("GET", "/api/cron/connections", async (_req, res) => {
2775
+ sendJson(res, 200, { connections: configStore.getCronConnections() });
2776
+ });
2777
+ server.route("POST", "/api/cron/connections", async (_req, res, _params, body) => {
2778
+ const input = body;
2779
+ if (!input?.cronJobId || !input?.targetAgent || !input?.command) {
2780
+ sendJson(res, 400, { error: "VALIDATION", message: "cronJobId, targetAgent, and command are required", statusCode: 400 });
2781
+ return;
2782
+ }
2783
+ if (!configStore.getCronJob(input.cronJobId)) {
2784
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Cron job not found", statusCode: 404 });
2785
+ return;
2786
+ }
2787
+ const connection = {
2788
+ id: randomUUID7(),
2789
+ cronJobId: input.cronJobId,
2790
+ targetAgent: input.targetAgent,
2791
+ command: input.command,
2792
+ enabled: true,
2793
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2794
+ };
2795
+ configStore.saveCronConnection(connection);
2796
+ const parentJob = configStore.getCronJob(input.cronJobId);
2797
+ if (parentJob) {
2798
+ generateCapabilityFile(agentsDir, connection.targetAgent, "cron", connection.id, buildCronCapabilityContent(parentJob, connection));
2799
+ }
2800
+ syncCronRunner(processManager, configStore);
2801
+ sendJson(res, 201, connection);
2802
+ });
2803
+ server.route("PUT", "/api/cron/connections/:id", async (_req, res, params, body) => {
2804
+ const existing = configStore.getCronConnections().find((c) => c.id === params["id"]);
2805
+ if (!existing) {
2806
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2807
+ return;
2808
+ }
2809
+ const updates = body ?? {};
2810
+ const updated = {
2811
+ ...existing,
2812
+ targetAgent: updates.targetAgent ?? existing.targetAgent,
2813
+ command: updates.command !== void 0 ? updates.command : existing.command,
2814
+ enabled: updates.enabled ?? existing.enabled
2815
+ };
2816
+ configStore.saveCronConnection(updated);
2817
+ const parentJob = configStore.getCronJob(updated.cronJobId);
2818
+ if (parentJob) {
2819
+ if (updates.targetAgent && updates.targetAgent !== existing.targetAgent) {
2820
+ removeCapabilityFile(agentsDir, existing.targetAgent, "cron", existing.id);
2821
+ }
2822
+ generateCapabilityFile(agentsDir, updated.targetAgent, "cron", updated.id, buildCronCapabilityContent(parentJob, updated));
2823
+ }
2824
+ syncCronRunner(processManager, configStore);
2825
+ sendJson(res, 200, updated);
2826
+ });
2827
+ server.route("DELETE", "/api/cron/connections/:id", async (_req, res, params) => {
2828
+ const existing = configStore.getCronConnections().find((c) => c.id === params["id"]);
2829
+ if (!existing) {
2830
+ sendJson(res, 404, { error: "NOT_FOUND", message: "Connection not found", statusCode: 404 });
2831
+ return;
2832
+ }
2833
+ configStore.deleteCronConnection(params["id"]);
2834
+ removeCapabilityFile(agentsDir, existing.targetAgent, "cron", existing.id);
2835
+ syncCronRunner(processManager, configStore);
2836
+ sendJson(res, 200, { deleted: true });
2837
+ });
2838
+ };
2839
+
2840
+ // ../daemon/src/daemon.ts
2841
+ var DEFAULT_PORT = 7777;
2842
+ var DEFAULT_WEB_PORT = 7001;
2843
+ var HUB_ID = "kindflow-hub";
2844
+ var startDaemon = async (options) => {
2845
+ const paths = resolvePaths(options?.paths);
2846
+ console.log("[daemon] Starting Kindflow daemon...");
2847
+ console.log(`[daemon] Agents dir: ${paths.agentsDir}`);
2848
+ console.log(`[daemon] Data dir: ${paths.dataDir}`);
2849
+ console.log(`[daemon] Runners dir: ${paths.runnersDir}`);
2850
+ const configStore = await createConfigStore(paths.dataDir);
2851
+ console.log("[daemon] Config store initialized");
2852
+ const configPort = parseInt(configStore.getSetting("daemon.port") ?? "", 10);
2853
+ const configWebPort = parseInt(configStore.getSetting("web.port") ?? "", 10);
2854
+ const port = options?.port ?? (configPort || DEFAULT_PORT);
2855
+ const webPort = options?.webPort ?? (configWebPort || DEFAULT_WEB_PORT);
2856
+ const apiServer = createApiServer();
2857
+ const ipcHub = createIpcHub({
2858
+ id: HUB_ID,
2859
+ onMessage: (msg) => {
2860
+ if (msg.to !== "*") {
2861
+ ipcHub.sendTo(msg.to, msg);
2862
+ } else {
2863
+ ipcHub.broadcast(msg);
2864
+ }
2865
+ }
2866
+ });
2867
+ await ipcHub.start();
2868
+ console.log("[daemon] IPC hub started");
2869
+ const processManager = createProcessManager({
2870
+ hubId: HUB_ID,
2871
+ runnersDir: paths.runnersDir,
2872
+ isDev: detectMode() === "dev",
2873
+ callbacks: {
2874
+ onStatusChange: (id, status, pid) => {
2875
+ apiServer.broadcast({ type: "process:status", id, status, pid });
2876
+ },
2877
+ onLog: (id, stream, data) => {
2878
+ apiServer.broadcast({ type: "process:log", id, stream, data });
2879
+ },
2880
+ onSdkMessage: (id, message) => {
2881
+ apiServer.broadcast({ type: "process:log", id, stream: "stdout", data: JSON.stringify(message) });
2882
+ }
2883
+ }
2884
+ });
2885
+ console.log("[daemon] Process manager initialized");
2886
+ const terminalManager = createTerminalManager({
2887
+ callbacks: {
2888
+ onOutput: (sessionId, data) => {
2889
+ apiServer.broadcast({ type: "terminal:output", sessionId, data });
2890
+ },
2891
+ onClose: (sessionId) => {
2892
+ apiServer.broadcast({ type: "terminal:closed", sessionId });
2893
+ }
2894
+ }
2895
+ });
2896
+ console.log("[daemon] Terminal manager initialized");
2897
+ registerAgentRoutes(apiServer, processManager, terminalManager, paths.agentsDir, paths.templateDir, configStore);
2898
+ registerProcessRoutes(apiServer, processManager, configStore, paths.agentsDir);
2899
+ registerS3Routes(apiServer, configStore, paths.agentsDir);
2900
+ registerEmailRoutes(apiServer, processManager, configStore, port, paths.agentsDir);
2901
+ registerDatabaseRoutes(apiServer, configStore, paths.agentsDir);
2902
+ registerHttpRoutes(apiServer, processManager, ipcHub, configStore, paths.agentsDir);
2903
+ registerTelegramRoutes(apiServer, processManager, configStore, paths.agentsDir);
2904
+ registerCronRoutes(apiServer, processManager, configStore, paths.agentsDir);
2905
+ apiServer.onWsCommand((_ws, command) => {
2906
+ switch (command.type) {
2907
+ case "terminal:ready":
2908
+ terminalManager.flush(command.sessionId);
2909
+ break;
2910
+ case "terminal:input":
2911
+ terminalManager.write(command.sessionId, command.data);
2912
+ break;
2913
+ case "terminal:resize":
2914
+ terminalManager.resize(command.sessionId, command.cols, command.rows);
2915
+ break;
2916
+ }
2917
+ });
2918
+ await apiServer.listen(port);
2919
+ const allAgents = scanAgents(paths.agentsDir);
2920
+ const agents = allAgents.filter((a) => a.config.autoStart);
2921
+ const helpers = configStore.getHelpers().filter((h) => h.autoStart);
2922
+ for (const agent of agents) {
2923
+ console.log(`[daemon] Auto-starting agent: ${agent.config.name}`);
2924
+ processManager.start(agent.id, "agent", agent);
2925
+ }
2926
+ for (const helper of helpers) {
2927
+ console.log(`[daemon] Auto-starting helper: ${helper.name}`);
2928
+ processManager.start(helper.id, "helper", helper);
2929
+ }
2930
+ console.log(`[daemon] Discovered ${allAgents.length} agent(s), auto-started ${agents.length}`);
2931
+ const emailAccounts = configStore.getEmailAccounts();
2932
+ const oauthCreds = configStore.getOAuthCredentials("gmail");
2933
+ if (emailAccounts.length > 0 && oauthCreds && !processManager.get("email-imap")) {
2934
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2935
+ const imapHelper = {
2936
+ id: "email-imap",
2937
+ kind: "email-imap",
2938
+ name: "Email IMAP Watcher",
2939
+ config: {
2940
+ accounts: emailAccounts,
2941
+ connections: configStore.getEmailConnections(),
2942
+ oauthCredentials: oauthCreds
2943
+ },
2944
+ autoStart: true,
2945
+ createdAt: now,
2946
+ updatedAt: now
2947
+ };
2948
+ configStore.saveHelper(imapHelper);
2949
+ console.log(`[daemon] Auto-starting email-imap helper (${emailAccounts.length} account(s))`);
2950
+ processManager.start("email-imap", "helper", imapHelper);
2951
+ }
2952
+ const telegramBots = configStore.getTelegramBots();
2953
+ const telegramConns = configStore.getTelegramConnections();
2954
+ if (telegramBots.length > 0 && telegramConns.length > 0 && !processManager.get("telegram")) {
2955
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2956
+ const telegramEncKey = getOrCreateKey2();
2957
+ const decryptedBots = telegramBots.map((b) => ({
2958
+ ...b,
2959
+ token: decrypt2(b.token, telegramEncKey)
2960
+ }));
2961
+ const telegramHelper = {
2962
+ id: "telegram",
2963
+ kind: "telegram",
2964
+ name: "Telegram Bot Runner",
2965
+ config: {
2966
+ bots: decryptedBots,
2967
+ connections: telegramConns
2968
+ },
2969
+ autoStart: true,
2970
+ createdAt: now,
2971
+ updatedAt: now
2972
+ };
2973
+ configStore.saveHelper(telegramHelper);
2974
+ console.log(`[daemon] Auto-starting telegram helper (${telegramBots.length} bot(s), ${telegramConns.length} connection(s))`);
2975
+ processManager.start("telegram", "helper", telegramHelper);
2976
+ }
2977
+ const cronRunnerJobs = buildCronRunnerConfig(configStore);
2978
+ if (cronRunnerJobs.length > 0 && !processManager.get("cron")) {
2979
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2980
+ const cronHelper = {
2981
+ id: "cron",
2982
+ kind: "cron",
2983
+ name: "Cron Runner",
2984
+ config: { cronJobs: cronRunnerJobs },
2985
+ autoStart: true,
2986
+ createdAt: now,
2987
+ updatedAt: now
2988
+ };
2989
+ configStore.saveHelper(cronHelper);
2990
+ console.log(`[daemon] Auto-starting cron-runner (${cronRunnerJobs.length} job config(s))`);
2991
+ processManager.start("cron", "helper", cronHelper);
2992
+ }
2993
+ let webServer = null;
2994
+ if (paths.webDir) {
2995
+ webServer = createWebServer({ port: webPort, webAppDir: paths.webDir });
2996
+ try {
2997
+ await webServer.start();
2998
+ console.log(`[daemon] Web UI: http://localhost:${webPort}`);
2999
+ } catch (err) {
3000
+ console.error(`[daemon] Failed to start web UI: ${err instanceof Error ? err.message : err}`);
3001
+ }
3002
+ }
3003
+ console.log(`[daemon] API: http://localhost:${port}`);
3004
+ const shutdown = async () => {
3005
+ console.log("[daemon] Shutting down...");
3006
+ webServer?.stop();
3007
+ terminalManager.closeAll();
3008
+ await processManager.shutdownAll();
3009
+ ipcHub.stop();
3010
+ await apiServer.close();
3011
+ configStore.close();
3012
+ console.log("[daemon] Shutdown complete");
3013
+ };
3014
+ return { configStore, processManager, terminalManager, ipcHub, apiServer, webServer, shutdown };
3015
+ };
9
3016
  export {
10
3017
  startDaemon
11
3018
  };