dis-gravity 1.1.0 → 1.2.5
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/.gitlab-ci.yml +9 -0
- package/.mcpbignore +20 -0
- package/README.md +471 -313
- package/SECURITY.md +105 -0
- package/bin/dis-gravity.js +1199 -702
- package/changelog.md +98 -0
- package/dist/extension.js +5630 -3610
- package/lib/core.js +1712 -0
- package/lib/onboarding.js +512 -0
- package/manifest.json +115 -0
- package/mcp/server.js +2174 -0
- package/media/mcp-icon.png +0 -0
- package/package.json +71 -16
- package/scripts/package-mcpb.js +29 -0
package/bin/dis-gravity.js
CHANGED
|
@@ -1,702 +1,1199 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dis-Gravity Standalone CLI Daemon
|
|
5
|
-
* Run Dis-Gravity as an editor-agnostic background service for JetBrains IDEs,
|
|
6
|
-
* Neovim, Emacs, Zed, or headless remote servers.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const cp = require('child_process');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
log
|
|
244
|
-
log
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
log
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dis-Gravity Standalone CLI Daemon
|
|
5
|
+
* Run Dis-Gravity as an editor-agnostic background service for JetBrains IDEs,
|
|
6
|
+
* Neovim, Emacs, Zed, or headless remote servers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const cp = require('child_process');
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
looksLikeBotToken,
|
|
16
|
+
buildInviteUrl,
|
|
17
|
+
inspectApplication,
|
|
18
|
+
ensureMessageContentIntent,
|
|
19
|
+
listUserGuilds,
|
|
20
|
+
waitForGuildJoin,
|
|
21
|
+
findOrCreateChannel,
|
|
22
|
+
sendWelcomeMessage,
|
|
23
|
+
loadConfig,
|
|
24
|
+
saveConfig,
|
|
25
|
+
clearConfig,
|
|
26
|
+
openBrowser,
|
|
27
|
+
CONFIG_FILE
|
|
28
|
+
} = require('../lib/onboarding');
|
|
29
|
+
const {
|
|
30
|
+
executeShellCommand,
|
|
31
|
+
findHeadlessBrowser,
|
|
32
|
+
buildTableHtml,
|
|
33
|
+
escapeHtml,
|
|
34
|
+
renderTableToPng,
|
|
35
|
+
isUserAllowed: coreIsUserAllowed,
|
|
36
|
+
parseMarkdownTable,
|
|
37
|
+
readWorkspaceFile,
|
|
38
|
+
listWorkspaceTree,
|
|
39
|
+
getGitStatusAndDiff,
|
|
40
|
+
getSystemHealth,
|
|
41
|
+
parseScheduleSpec,
|
|
42
|
+
parseScheduleCommandInput,
|
|
43
|
+
TaskScheduler,
|
|
44
|
+
ThreadWorkspaceManager,
|
|
45
|
+
isAudioAttachment,
|
|
46
|
+
transcribeAudioFile,
|
|
47
|
+
buildDiffHtml,
|
|
48
|
+
renderDiffToPng,
|
|
49
|
+
detectDestructiveKeywords,
|
|
50
|
+
captureWorkspaceCheckpoint,
|
|
51
|
+
restoreWorkspaceCheckpoint
|
|
52
|
+
} = require('../lib/core');
|
|
53
|
+
|
|
54
|
+
function parseArgs() {
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
const saved = loadConfig();
|
|
57
|
+
|
|
58
|
+
const config = {
|
|
59
|
+
token: process.env.DISCORD_BOT_TOKEN || saved.token || '',
|
|
60
|
+
channelId: process.env.DISCORD_CHANNEL_ID || saved.channelId || '',
|
|
61
|
+
guildId: process.env.DISCORD_GUILD_ID || saved.guildId || '',
|
|
62
|
+
allowedUsers: process.env.DISCORD_ALLOWED_USERS
|
|
63
|
+
? process.env.DISCORD_ALLOWED_USERS.split(',').map(s => s.trim()).filter(Boolean)
|
|
64
|
+
: (Array.isArray(saved.allowedUsers) ? saved.allowedUsers : []),
|
|
65
|
+
provider: process.env.DISCORD_AGENT_PROVIDER || saved.provider || 'cli',
|
|
66
|
+
debugPort: parseInt(process.env.DEBUG_PORT || String(saved.debugPort || '9000'), 10),
|
|
67
|
+
workspace: process.cwd(),
|
|
68
|
+
tableStyle: process.env.TABLE_STYLE || saved.tableStyle || 'image',
|
|
69
|
+
autoAttachTableImages: true,
|
|
70
|
+
runSetup: false,
|
|
71
|
+
showStatus: false
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < args.length; i++) {
|
|
75
|
+
const arg = args[i];
|
|
76
|
+
if (arg === '--help' || arg === '-h') {
|
|
77
|
+
printHelp();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
if (arg === 'setup' || arg === '--setup') {
|
|
81
|
+
config.runSetup = true;
|
|
82
|
+
} else if (arg === 'status' || arg === '--status') {
|
|
83
|
+
config.showStatus = true;
|
|
84
|
+
} else if (arg === '--reset') {
|
|
85
|
+
clearConfig();
|
|
86
|
+
console.log(`✅ Dis-Gravity configuration cleared (~/.dis-gravity/config.json deleted).`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
} else if (arg === '--token' || arg === '-t') config.token = args[++i] || '';
|
|
89
|
+
else if (arg.startsWith('--token=')) config.token = arg.split('=')[1] || '';
|
|
90
|
+
else if (arg === '--channel' || arg === '-c') config.channelId = args[++i] || '';
|
|
91
|
+
else if (arg.startsWith('--channel=')) config.channelId = arg.split('=')[1] || '';
|
|
92
|
+
else if (arg === '--guild' || arg === '-g') config.guildId = args[++i] || '';
|
|
93
|
+
else if (arg.startsWith('--guild=')) config.guildId = arg.split('=')[1] || '';
|
|
94
|
+
else if (arg === '--provider' || arg === '-p') config.provider = args[++i] || 'cli';
|
|
95
|
+
else if (arg.startsWith('--provider=')) config.provider = arg.split('=')[1] || 'cli';
|
|
96
|
+
else if (arg === '--port') config.debugPort = parseInt(args[++i] || '9000', 10);
|
|
97
|
+
else if (arg.startsWith('--port=')) config.debugPort = parseInt(arg.split('=')[1] || '9000', 10);
|
|
98
|
+
else if (arg === '--workspace' || arg === '-w') {
|
|
99
|
+
if (args[i + 1]) config.workspace = path.resolve(args[++i]);
|
|
100
|
+
} else if (arg.startsWith('--workspace=')) {
|
|
101
|
+
const wsVal = arg.split('=')[1];
|
|
102
|
+
if (wsVal) config.workspace = path.resolve(wsVal);
|
|
103
|
+
} else if (arg === '--allowed-users' || arg === '-u') {
|
|
104
|
+
const uVal = args[++i] || '';
|
|
105
|
+
config.allowedUsers = uVal.split(',').map(s => s.trim()).filter(Boolean);
|
|
106
|
+
} else if (arg.startsWith('--allowed-users=')) {
|
|
107
|
+
const uVal = arg.split('=')[1] || '';
|
|
108
|
+
config.allowedUsers = uVal.split(',').map(s => s.trim()).filter(Boolean);
|
|
109
|
+
} else if (arg === '--table-style') config.tableStyle = args[++i] || 'image';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printHelp() {
|
|
116
|
+
console.log(`
|
|
117
|
+
╔════════════════════════════════════════════════════════════════════════╗
|
|
118
|
+
║ Dis-Gravity Standalone CLI Daemon ║
|
|
119
|
+
║ The Ultimate Discord Bridge for JetBrains, Neovim & Headless ║
|
|
120
|
+
╚════════════════════════════════════════════════════════════════════════╝
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
npx dis-gravity [command] [options]
|
|
124
|
+
node bin/dis-gravity.js [command] [options]
|
|
125
|
+
|
|
126
|
+
Commands:
|
|
127
|
+
setup, --setup Run interactive guided onboarding wizard (<90 seconds)
|
|
128
|
+
status, --status Show current configuration and saved credentials
|
|
129
|
+
--reset Delete saved credentials (~/.dis-gravity/config.json)
|
|
130
|
+
|
|
131
|
+
Options:
|
|
132
|
+
-t, --token <string> Discord Bot Token (env: DISCORD_BOT_TOKEN)
|
|
133
|
+
-c, --channel <string> Discord Channel ID to bind (env: DISCORD_CHANNEL_ID)
|
|
134
|
+
-g, --guild <string> Discord Guild ID (optional, for instant slash commands)
|
|
135
|
+
-p, --provider <string> AI Provider: 'cli', 'antigravity', 'cursor', 'windsurf' (default: cli)
|
|
136
|
+
-w, --workspace <path> Workspace folder path (default: current directory)
|
|
137
|
+
-u, --allowed-users <ids> Comma-separated Discord User IDs allowed to execute commands
|
|
138
|
+
--port <number> CDP port for Antigravity/Cursor/Windsurf (default: 9000)
|
|
139
|
+
--table-style <style> Table formatting: image, cards, rounded, boxed, ascii (default: image)
|
|
140
|
+
-h, --help Show this help menu
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
npx dis-gravity setup
|
|
144
|
+
npx dis-gravity
|
|
145
|
+
npx dis-gravity --provider=antigravity --port=9000
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Minimal logger
|
|
150
|
+
const log = {
|
|
151
|
+
appendLine: (msg) => {
|
|
152
|
+
const ts = new Date().toISOString().substring(11, 19);
|
|
153
|
+
console.log(`[${ts}] ${msg}`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
function promptUser(query, isPassword = false) {
|
|
160
|
+
const readline = require('readline');
|
|
161
|
+
const rl = readline.createInterface({
|
|
162
|
+
input: process.stdin,
|
|
163
|
+
output: process.stdout
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
if (isPassword && process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
168
|
+
process.stdout.write(query);
|
|
169
|
+
let input = '';
|
|
170
|
+
const onData = (char) => {
|
|
171
|
+
char = char.toString();
|
|
172
|
+
if (char === '\n' || char === '\r' || char === '\u0004') {
|
|
173
|
+
process.stdin.removeListener('data', onData);
|
|
174
|
+
try { process.stdin.setRawMode(false); } catch { }
|
|
175
|
+
process.stdout.write('\n');
|
|
176
|
+
rl.close();
|
|
177
|
+
resolve(input.trim());
|
|
178
|
+
} else if (char === '\u0003') {
|
|
179
|
+
process.exit(1);
|
|
180
|
+
} else if (char === '\b' || char === '\x7f') {
|
|
181
|
+
if (input.length > 0) {
|
|
182
|
+
input = input.slice(0, -1);
|
|
183
|
+
process.stdout.write('\b \b');
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
input += char;
|
|
187
|
+
process.stdout.write('*');
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
process.stdin.setRawMode(true);
|
|
191
|
+
process.stdin.resume();
|
|
192
|
+
process.stdin.on('data', onData);
|
|
193
|
+
} else {
|
|
194
|
+
rl.question(query, (ans) => {
|
|
195
|
+
rl.close();
|
|
196
|
+
resolve(ans.trim());
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function runCliSetupWizard(config) {
|
|
203
|
+
console.log(`
|
|
204
|
+
╔════════════════════════════════════════════════════════════════════════╗
|
|
205
|
+
║ Dis-Gravity Guided Onboarding Wizard ║
|
|
206
|
+
║ Zero-Friction Discord Bridge Setup in under 90s ║
|
|
207
|
+
╚════════════════════════════════════════════════════════════════════════╝
|
|
208
|
+
`);
|
|
209
|
+
|
|
210
|
+
console.log(`Dis-Gravity bridges your terminal and AI coding agents to Discord.\n`);
|
|
211
|
+
console.log(`Step 1 of 4: Discord Bot Token`);
|
|
212
|
+
console.log(`────────────────────────────────────────────────────────────────────────`);
|
|
213
|
+
console.log(`If you haven't created a bot yet:`);
|
|
214
|
+
console.log(` 1. Open Discord Developer Portal: https://discord.com/developers/applications`);
|
|
215
|
+
console.log(` 2. Click 'New Application' -> Name it 'Dis-Gravity' (or any name)`);
|
|
216
|
+
console.log(` 3. Click 'Bot' in sidebar -> Click 'Reset Token' -> Copy Token\n`);
|
|
217
|
+
|
|
218
|
+
let token = config.token;
|
|
219
|
+
while (!token || !looksLikeBotToken(token)) {
|
|
220
|
+
if (token && !looksLikeBotToken(token)) {
|
|
221
|
+
console.log(`⚠️ That doesn't look like a valid Discord bot token format.`);
|
|
222
|
+
}
|
|
223
|
+
const openPortal = await promptUser(`Would you like to open the Developer Portal in your browser? [Y/n]: `);
|
|
224
|
+
if (!openPortal || /^y/i.test(openPortal)) {
|
|
225
|
+
openBrowser('https://discord.com/developers/applications');
|
|
226
|
+
console.log(`🌐 Opened https://discord.com/developers/applications in browser.\n`);
|
|
227
|
+
}
|
|
228
|
+
token = await promptUser(`Paste your Bot Token: `, true);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(`\n🔍 Verifying token and inspecting application...`);
|
|
232
|
+
let appInfo;
|
|
233
|
+
try {
|
|
234
|
+
appInfo = await inspectApplication(token);
|
|
235
|
+
console.log(`✅ Verified Bot: @${appInfo.botTag} (App: "${appInfo.appName}")`);
|
|
236
|
+
console.log(` Owner: ${appInfo.ownerUsername} (ID: ${appInfo.ownerId})`);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error(`❌ Failed to verify bot token with Discord API: ${err.message}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(`\nStep 2 of 4: Message Content Intent`);
|
|
243
|
+
console.log(`────────────────────────────────────────────────────────────────────────`);
|
|
244
|
+
console.log(`Checking Message Content Intent (required to read commands)...`);
|
|
245
|
+
const intentRes = await ensureMessageContentIntent(token);
|
|
246
|
+
if (intentRes.alreadyEnabled) {
|
|
247
|
+
console.log(`✅ Message Content Intent is already enabled.`);
|
|
248
|
+
} else if (intentRes.autoEnabled) {
|
|
249
|
+
console.log(`✅ Automatically enabled Message Content Intent via Discord API.`);
|
|
250
|
+
} else {
|
|
251
|
+
console.log(`⚠️ Could not auto-enable intent via API.`);
|
|
252
|
+
console.log(` Please open: ${intentRes.portalUrl}`);
|
|
253
|
+
console.log(` Scroll to "Privileged Gateway Intents" -> Toggle "Message Content Intent" -> Save Changes.`);
|
|
254
|
+
await promptUser(`Press Enter once Message Content Intent is toggled ON... `);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log(`\nStep 3 of 4: Server Authorization`);
|
|
258
|
+
console.log(`────────────────────────────────────────────────────────────────────────`);
|
|
259
|
+
let guilds = await listUserGuilds(token).catch(() => []);
|
|
260
|
+
let selectedGuild = null;
|
|
261
|
+
|
|
262
|
+
if (guilds.length > 0) {
|
|
263
|
+
console.log(`The bot is already in the following server(s):`);
|
|
264
|
+
guilds.forEach((g, idx) => console.log(` [${idx + 1}] ${g.name} (ID: ${g.id})`));
|
|
265
|
+
console.log(` [N] Invite bot to a different server`);
|
|
266
|
+
const choice = await promptUser(`Select server [1-${guilds.length}] or N: `);
|
|
267
|
+
const num = parseInt(choice, 10);
|
|
268
|
+
if (num >= 1 && num <= guilds.length) {
|
|
269
|
+
selectedGuild = guilds[num - 1];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!selectedGuild) {
|
|
274
|
+
const inviteUrl = buildInviteUrl(appInfo.appId);
|
|
275
|
+
console.log(`Opening OAuth2 server invite in your browser:`);
|
|
276
|
+
console.log(`🔗 ${inviteUrl}\n`);
|
|
277
|
+
openBrowser(inviteUrl);
|
|
278
|
+
console.log(`⏳ Waiting for bot to join your server (click 'Authorize' on Discord)...`);
|
|
279
|
+
const initialGuildIds = guilds.map(g => g.id);
|
|
280
|
+
try {
|
|
281
|
+
selectedGuild = await waitForGuildJoin(token, {
|
|
282
|
+
initialGuildIds,
|
|
283
|
+
timeoutMs: 120000,
|
|
284
|
+
onPoll: ({ elapsedMs }) => {
|
|
285
|
+
process.stdout.write(`\rWaiting for authorization... (${Math.round(elapsedMs / 1000)}s) `);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
process.stdout.write('\n');
|
|
289
|
+
console.log(`✅ Bot joined server: "${selectedGuild.name}"!`);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error(`\n❌ ${err.message}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
console.log(`Using server: "${selectedGuild.name}".`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(`\nStep 4 of 4: Private Channel & Security Setup`);
|
|
299
|
+
console.log(`────────────────────────────────────────────────────────────────────────`);
|
|
300
|
+
let channelResult;
|
|
301
|
+
try {
|
|
302
|
+
channelResult = await findOrCreateChannel(token, selectedGuild.id, {
|
|
303
|
+
botId: appInfo.botId,
|
|
304
|
+
ownerId: appInfo.ownerId,
|
|
305
|
+
channelName: 'dis-gravity'
|
|
306
|
+
});
|
|
307
|
+
if (channelResult.created) {
|
|
308
|
+
console.log(`✅ Created private channel #${channelResult.channel.name} (ID: ${channelResult.channel.id})`);
|
|
309
|
+
console.log(` (Strictly locked to @everyone; accessible only by you and your bot)`);
|
|
310
|
+
} else {
|
|
311
|
+
console.log(`✅ Reusing existing #${channelResult.channel.name} channel (ID: ${channelResult.channel.id})`);
|
|
312
|
+
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err.code === 'MISSING_MANAGE_CHANNELS' && err.fallbackChannel) {
|
|
315
|
+
console.log(`⚠️ ${err.message}`);
|
|
316
|
+
channelResult = { channel: err.fallbackChannel };
|
|
317
|
+
} else {
|
|
318
|
+
console.error(`❌ Failed channel setup: ${err.message}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Security whitelist
|
|
324
|
+
const allowedUsers = appInfo.ownerId ? [appInfo.ownerId] : [];
|
|
325
|
+
console.log(`🔒 Auto-whitelisted your Discord User ID (${appInfo.ownerId}) for command authorization.`);
|
|
326
|
+
|
|
327
|
+
// Save config
|
|
328
|
+
saveConfig({
|
|
329
|
+
token,
|
|
330
|
+
channelId: channelResult.channel.id,
|
|
331
|
+
guildId: selectedGuild.id,
|
|
332
|
+
allowedUsers,
|
|
333
|
+
provider: config.provider || 'cli',
|
|
334
|
+
tableStyle: config.tableStyle || 'image'
|
|
335
|
+
});
|
|
336
|
+
console.log(`💾 Configuration saved securely to ${CONFIG_FILE} (0600 mode).`);
|
|
337
|
+
|
|
338
|
+
// Send welcome ping
|
|
339
|
+
console.log(`📬 Sending welcome notification to #${channelResult.channel.name}...`);
|
|
340
|
+
await sendWelcomeMessage(token, channelResult.channel.id, {
|
|
341
|
+
ownerId: appInfo.ownerId,
|
|
342
|
+
provider: config.provider || 'cli'
|
|
343
|
+
}).catch(() => { });
|
|
344
|
+
|
|
345
|
+
console.log(`\n🎉 Setup complete! Launching Dis-Gravity daemon...\n`);
|
|
346
|
+
|
|
347
|
+
config.token = token;
|
|
348
|
+
config.channelId = channelResult.channel.id;
|
|
349
|
+
config.guildId = selectedGuild.id;
|
|
350
|
+
config.allowedUsers = allowedUsers;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Main Daemon Application
|
|
354
|
+
async function startDaemon() {
|
|
355
|
+
const config = parseArgs();
|
|
356
|
+
|
|
357
|
+
if (config.showStatus) {
|
|
358
|
+
console.log(`\n📊 Dis-Gravity CLI Status & Configuration`);
|
|
359
|
+
console.log(`• Config File: ${fs.existsSync(CONFIG_FILE) ? CONFIG_FILE : 'None (not configured)'}`);
|
|
360
|
+
console.log(`• Bot Token: ${config.token ? (config.token.substring(0, 10) + '...' + config.token.slice(-4)) : 'Not set'}`);
|
|
361
|
+
console.log(`• Channel ID: ${config.channelId || 'Not set'}`);
|
|
362
|
+
console.log(`• Guild ID: ${config.guildId || 'Not set'}`);
|
|
363
|
+
console.log(`• Allowed Users: ${config.allowedUsers.length > 0 ? config.allowedUsers.join(', ') : 'None'}`);
|
|
364
|
+
console.log(`• Provider: ${config.provider}`);
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (config.runSetup || !config.token || !config.channelId) {
|
|
369
|
+
if (process.stdin.isTTY) {
|
|
370
|
+
await runCliSetupWizard(config);
|
|
371
|
+
} else {
|
|
372
|
+
console.error('❌ Error: Discord Bot Token and Channel ID are required.');
|
|
373
|
+
console.error(' Run `npx dis-gravity setup` interactively, or pass --token and --channel flags.');
|
|
374
|
+
console.error(' Alternatively, configure ~/.dis-gravity/config.json.');
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let Client, GatewayIntentBits, Partials, ActionRowBuilder, ButtonBuilder, ButtonStyle;
|
|
380
|
+
try {
|
|
381
|
+
const djs = require('discord.js');
|
|
382
|
+
Client = djs.Client;
|
|
383
|
+
GatewayIntentBits = djs.GatewayIntentBits;
|
|
384
|
+
Partials = djs.Partials;
|
|
385
|
+
ActionRowBuilder = djs.ActionRowBuilder;
|
|
386
|
+
ButtonBuilder = djs.ButtonBuilder;
|
|
387
|
+
ButtonStyle = djs.ButtonStyle;
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error('❌ Error: discord.js is required to run the standalone daemon.');
|
|
390
|
+
console.error(' Please install it with: npm install discord.js');
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log(`
|
|
395
|
+
╔════════════════════════════════════════════════════════════════════════╗
|
|
396
|
+
║ Dis-Gravity Standalone CLI Daemon ║
|
|
397
|
+
╚════════════════════════════════════════════════════════════════════════╝
|
|
398
|
+
`);
|
|
399
|
+
|
|
400
|
+
log.appendLine(`Initializing Daemon with workspace: ${config.workspace}`);
|
|
401
|
+
log.appendLine(`Provider: ${config.provider.toUpperCase()} | Channel ID: ${config.channelId}`);
|
|
402
|
+
if (config.allowedUsers.length > 0) {
|
|
403
|
+
log.appendLine(`Security Whitelist: ${config.allowedUsers.length} user(s) allowed (${config.allowedUsers.join(', ')})`);
|
|
404
|
+
} else {
|
|
405
|
+
log.appendLine(`⚠️ SECURITY NOTICE: No --allowed-users configured! Default-deny security is active.`);
|
|
406
|
+
log.appendLine(` All remote commands and interactions are BLOCKED until you specify authorized Discord User ID(s).`);
|
|
407
|
+
log.appendLine(` Configure with: --allowed-users="YOUR_DISCORD_USER_ID" or set "allowedUsers" in dis-gravity.config.json.`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const client = new Client({
|
|
411
|
+
intents: [
|
|
412
|
+
GatewayIntentBits.Guilds,
|
|
413
|
+
GatewayIntentBits.GuildMessages,
|
|
414
|
+
GatewayIntentBits.MessageContent,
|
|
415
|
+
GatewayIntentBits.DirectMessages
|
|
416
|
+
],
|
|
417
|
+
partials: [Partials.Channel, Partials.Message]
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const isUserAllowed = (userId) => coreIsUserAllowed(userId, config.allowedUsers);
|
|
421
|
+
|
|
422
|
+
// Thread-per-Session Workspace Manager & Task Scheduler
|
|
423
|
+
const threadStoragePath = path.join(os.homedir(), '.dis-gravity', 'thread-workspaces.json');
|
|
424
|
+
const threadWorkspaces = new ThreadWorkspaceManager({ storageFile: threadStoragePath });
|
|
425
|
+
const scheduler = new TaskScheduler({ maxJobs: 25 });
|
|
426
|
+
|
|
427
|
+
const resolveThreadWorkspace = (channelObj) => {
|
|
428
|
+
if (channelObj && typeof channelObj.isThread === 'function' && channelObj.isThread()) {
|
|
429
|
+
return threadWorkspaces.getThreadWorkspace(channelObj.id, config.workspace);
|
|
430
|
+
}
|
|
431
|
+
return config.workspace;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// Active CLI agent supervisor
|
|
435
|
+
let activeProcess = null;
|
|
436
|
+
let activeStepStatus = null;
|
|
437
|
+
let pendingApproval = null;
|
|
438
|
+
let approvalResolver = null;
|
|
439
|
+
|
|
440
|
+
async function cancelActiveAgent() {
|
|
441
|
+
if (activeProcess) {
|
|
442
|
+
try { activeProcess.kill('SIGINT'); } catch { }
|
|
443
|
+
activeProcess = null;
|
|
444
|
+
}
|
|
445
|
+
activeStepStatus = null;
|
|
446
|
+
if (approvalResolver) {
|
|
447
|
+
approvalResolver('cancel');
|
|
448
|
+
approvalResolver = null;
|
|
449
|
+
pendingApproval = null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function executeCliAgent(promptText, onStepUpdate, targetWorkspace = config.workspace) {
|
|
454
|
+
const cwd = targetWorkspace || config.workspace;
|
|
455
|
+
const isWindows = process.platform === 'win32';
|
|
456
|
+
const checkCmd = isWindows ? 'where.exe' : 'which';
|
|
457
|
+
|
|
458
|
+
let engine = 'shell';
|
|
459
|
+
const check = (bin) => {
|
|
460
|
+
try { cp.execFileSync(checkCmd, [bin], { stdio: 'ignore' }); return true; } catch { return false; }
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
if (check('agy')) engine = 'agy';
|
|
464
|
+
else if (check('claude')) engine = 'claude';
|
|
465
|
+
else if (check('aider')) engine = 'aider';
|
|
466
|
+
|
|
467
|
+
let binary = '';
|
|
468
|
+
let args = [];
|
|
469
|
+
|
|
470
|
+
if (engine === 'agy') {
|
|
471
|
+
binary = 'agy';
|
|
472
|
+
args = [promptText];
|
|
473
|
+
} else if (engine === 'claude') {
|
|
474
|
+
binary = 'claude';
|
|
475
|
+
args = ['-p', promptText];
|
|
476
|
+
} else if (engine === 'aider') {
|
|
477
|
+
binary = 'aider';
|
|
478
|
+
args = ['--message', promptText, '--yes-always'];
|
|
479
|
+
} else {
|
|
480
|
+
binary = isWindows ? 'powershell.exe' : '/bin/bash';
|
|
481
|
+
args = isWindows ? ['-NoProfile', '-Command', promptText] : ['-c', promptText];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
log.appendLine(`[Agent] Spawning ${binary} (${engine.toUpperCase()}) in ${cwd}...`);
|
|
485
|
+
activeStepStatus = `Running ${engine.toUpperCase()} agent...`;
|
|
486
|
+
onStepUpdate?.(activeStepStatus);
|
|
487
|
+
|
|
488
|
+
return new Promise((resolve) => {
|
|
489
|
+
let stdout = '';
|
|
490
|
+
let stderr = '';
|
|
491
|
+
const startTime = Date.now();
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const proc = cp.spawn(binary, args, {
|
|
495
|
+
cwd,
|
|
496
|
+
env: { ...process.env, CI: '1', TERM: 'dumb' }
|
|
497
|
+
});
|
|
498
|
+
activeProcess = proc;
|
|
499
|
+
|
|
500
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
501
|
+
|
|
502
|
+
const MAX_OUTPUT_BUF = 64 * 1024;
|
|
503
|
+
proc.stdout.on('data', (chunk) => {
|
|
504
|
+
const text = stripAnsi(chunk.toString());
|
|
505
|
+
if (stdout.length < MAX_OUTPUT_BUF) {
|
|
506
|
+
stdout += text.slice(0, MAX_OUTPUT_BUF - stdout.length);
|
|
507
|
+
}
|
|
508
|
+
const lastLine = text.trim().split('\n').pop() || '';
|
|
509
|
+
if (lastLine) {
|
|
510
|
+
activeStepStatus = lastLine.substring(0, 80);
|
|
511
|
+
onStepUpdate?.(activeStepStatus);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (/(?:\(|\[)\s*[yY]\s*[\/|]\s*[nN]\s*(?:\)|\])|(?:proceed|allow|continue|confirm|choice|run this)\b.*\?|\?\s+\[[yY]\/[nN]\]/i.test(lastLine)) {
|
|
515
|
+
pendingApproval = {
|
|
516
|
+
question: lastLine,
|
|
517
|
+
options: [
|
|
518
|
+
{ id: 'yes', text: '🟢 Proceed / Allow (y)' },
|
|
519
|
+
{ id: 'no', text: '🔴 Deny / Cancel (n)' }
|
|
520
|
+
]
|
|
521
|
+
};
|
|
522
|
+
approvalResolver = (ans) => {
|
|
523
|
+
const isYes = /^(yes|proceed|allow|y)/i.test(ans || '');
|
|
524
|
+
try { proc.stdin.write(isYes ? 'y\n' : 'n\n'); } catch { }
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
proc.stderr.on('data', (chunk) => {
|
|
530
|
+
const text = stripAnsi(chunk.toString());
|
|
531
|
+
if (stderr.length < MAX_OUTPUT_BUF) {
|
|
532
|
+
stderr += text.slice(0, MAX_OUTPUT_BUF - stderr.length);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
proc.on('close', (code) => {
|
|
537
|
+
activeProcess = null;
|
|
538
|
+
activeStepStatus = null;
|
|
539
|
+
pendingApproval = null;
|
|
540
|
+
approvalResolver = null;
|
|
541
|
+
const dur = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
542
|
+
const badge = code === 0 ? `✅ **Process completed in ${dur}s (exit 0)**` : `⚠️ **Process exited with code ${code} (${dur}s)**`;
|
|
543
|
+
const output = (stdout.trim() || stderr.trim()) || '(No output recorded)';
|
|
544
|
+
resolve(`${badge}\n\n\`\`\`text\n${output.substring(0, 1800)}\n\`\`\``);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
proc.on('error', (err) => {
|
|
548
|
+
activeProcess = null;
|
|
549
|
+
activeStepStatus = null;
|
|
550
|
+
resolve(`❌ **Failed to spawn CLI process (${binary}):** ${err.message}`);
|
|
551
|
+
});
|
|
552
|
+
} catch (err) {
|
|
553
|
+
activeProcess = null;
|
|
554
|
+
activeStepStatus = null;
|
|
555
|
+
resolve(`❌ **Execution error:** ${err.message}`);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function buildScheduleListPayload() {
|
|
561
|
+
const jobs = scheduler.listJobs();
|
|
562
|
+
if (jobs.length === 0) {
|
|
563
|
+
return {
|
|
564
|
+
content: `⏰ **Scheduled Prompts (0 active)**\n\nNo scheduled tasks are currently queued.\n💡 Schedule one with: \`/schedule time:30m prompt:run npm test\` or \`!schedule every 1h check git status\``,
|
|
565
|
+
components: []
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let msg = `⏰ **Active Scheduled Prompts (${jobs.length}):**\n\n`;
|
|
570
|
+
for (const j of jobs) {
|
|
571
|
+
const nextStr = j.nextRunAt ? new Date(j.nextRunAt).toLocaleTimeString() : 'N/A';
|
|
572
|
+
const wsBadge = j.workspace ? ` • 📁 \`${path.basename(j.workspace)}\`` : '';
|
|
573
|
+
msg += `• **#${j.id}** [\`${j.spec}\` — ${j.description}] → \`${j.prompt.substring(0, 80)}\` (Next: ${nextStr}${wsBadge})\n`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const row = new ActionRowBuilder();
|
|
577
|
+
for (const j of jobs.slice(0, 4)) {
|
|
578
|
+
row.addComponents(
|
|
579
|
+
new ButtonBuilder()
|
|
580
|
+
.setCustomId(`schedule:cancel:${j.id}`)
|
|
581
|
+
.setLabel(`🗑️ Cancel #${j.id}`)
|
|
582
|
+
.setStyle(ButtonStyle.Secondary)
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
row.addComponents(
|
|
586
|
+
new ButtonBuilder()
|
|
587
|
+
.setCustomId('schedule:cancel_all')
|
|
588
|
+
.setLabel('🧹 Cancel All')
|
|
589
|
+
.setStyle(ButtonStyle.Danger)
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
return { content: msg.substring(0, 1900), components: [row] };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function triggerScheduledJob(job) {
|
|
596
|
+
try {
|
|
597
|
+
const targetChannelId = job.threadId || config.channelId;
|
|
598
|
+
const targetChannel = await client.channels.fetch(targetChannelId).catch(() => null);
|
|
599
|
+
if (!targetChannel) return;
|
|
600
|
+
const targetWs = job.workspace || config.workspace;
|
|
601
|
+
const statusMsg = await targetChannel.send(`⏰ **Running Scheduled Task #${job.id}** (\`${job.spec}\` | 📁 \`${path.basename(targetWs)}\`):\n> ${job.prompt}`);
|
|
602
|
+
const res = await executeCliAgent(job.prompt, null, targetWs);
|
|
603
|
+
await statusMsg.edit(`⏰ **Scheduled Task #${job.id} Result** (\`${job.spec}\`):\n> ${job.prompt}\n\n${res}`).catch(() => { });
|
|
604
|
+
} catch (err) {
|
|
605
|
+
log.appendLine(`[Schedule] Error running job #${job.id}: ${err.message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function getDaemonStatusPayload(customHeader = null, targetWorkspace = config.workspace) {
|
|
610
|
+
const health = getSystemHealth(targetWorkspace);
|
|
611
|
+
const gitInfo = await getGitStatusAndDiff(targetWorkspace);
|
|
612
|
+
const activeSchedules = scheduler.listJobs().length;
|
|
613
|
+
const activeBindings = threadWorkspaces.listBindings().length;
|
|
614
|
+
|
|
615
|
+
let msg = "";
|
|
616
|
+
if (customHeader) {
|
|
617
|
+
msg += `${customHeader}\n\n`;
|
|
618
|
+
}
|
|
619
|
+
msg += `📊 **Dis-Gravity Standalone Daemon Status**\n\n`;
|
|
620
|
+
msg += `• **Project:** 📁 \`${path.basename(targetWorkspace)}\`\n`;
|
|
621
|
+
msg += `• **Workspace:** \`${targetWorkspace}\`\n`;
|
|
622
|
+
msg += `• **Git Branch:** 🌿 \`${gitInfo.branch}\`\n`;
|
|
623
|
+
msg += `• **Provider:** 🤖 🟢 **Standalone Daemon (${config.provider.toUpperCase()})**\n`;
|
|
624
|
+
msg += `• **Daemon State:** ${activeProcess ? '⏳ Busy (Process active)' : '🟢 Idle (Waiting for commands)'}\n`;
|
|
625
|
+
msg += `• **Schedules / Threads:** ⏰ ${activeSchedules} scheduled | 🧵 ${activeBindings} bound thread(s)\n`;
|
|
626
|
+
msg += `• **Host System:** 💻 ${health.platform} (${health.release}, ${health.arch})\n`;
|
|
627
|
+
msg += `• **Memory:** 📈 ${health.memory}\n`;
|
|
628
|
+
if (!config.allowedUsers || config.allowedUsers.length === 0) {
|
|
629
|
+
msg += `\n> 🛡️ *Security active: No \`allowedUsers\` configured. Remote execution is BLOCKED until your Discord User ID is added.*`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const actionRow = new ActionRowBuilder().addComponents(
|
|
633
|
+
new ButtonBuilder()
|
|
634
|
+
.setCustomId("daemon:status")
|
|
635
|
+
.setLabel("🔄 Refresh")
|
|
636
|
+
.setStyle(ButtonStyle.Primary),
|
|
637
|
+
new ButtonBuilder()
|
|
638
|
+
.setCustomId("daemon:diff")
|
|
639
|
+
.setLabel("🌿 Git Diff")
|
|
640
|
+
.setStyle(ButtonStyle.Secondary),
|
|
641
|
+
new ButtonBuilder()
|
|
642
|
+
.setCustomId("daemon:info")
|
|
643
|
+
.setLabel("🖥️ System Info")
|
|
644
|
+
.setStyle(ButtonStyle.Secondary)
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
return { content: msg.substring(0, 1900), components: [actionRow] };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function sendDaemonStatusNotification(channel, header = "🚀 **Dis-Gravity Daemon Connected & Ready**") {
|
|
651
|
+
try {
|
|
652
|
+
const payload = await getDaemonStatusPayload(header);
|
|
653
|
+
await channel.send(payload);
|
|
654
|
+
log.appendLine(`Sent automatic status notification to #${channel.name || channel.id}`);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
log.appendLine(`Failed to send daemon status notification: ${err.message}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
client.once('ready', async () => {
|
|
661
|
+
log.appendLine(`🚀 Dis-Gravity Daemon logged in as ${client.user.tag}`);
|
|
662
|
+
const channel = await client.channels.fetch(config.channelId).catch(() => null);
|
|
663
|
+
if (!channel) {
|
|
664
|
+
log.appendLine(`⚠️ Warning: Could not find channel with ID ${config.channelId}`);
|
|
665
|
+
} else {
|
|
666
|
+
log.appendLine(`Connected to Discord Channel: #${channel.name || config.channelId}`);
|
|
667
|
+
await sendDaemonStatusNotification(channel, "🚀 **Dis-Gravity Daemon Connected & Ready**");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Register slash commands
|
|
671
|
+
try {
|
|
672
|
+
const slashCommands = [
|
|
673
|
+
{ name: "sh", description: "Execute shell command in workspace", options: [{ name: "command", description: "Command to execute", type: 3, required: true }] },
|
|
674
|
+
{ name: "diff", description: "View git diff summary and preview" },
|
|
675
|
+
{ name: "info", description: "View host hardware and workspace health dashboard" },
|
|
676
|
+
{ name: "tree", description: "View workspace directory tree" },
|
|
677
|
+
{
|
|
678
|
+
name: "schedule",
|
|
679
|
+
description: "Schedule a delayed or recurring cron prompt, or list/cancel active schedules",
|
|
680
|
+
options: [
|
|
681
|
+
{ name: "time", description: "Delay or cron (e.g. '30m', 'every 1h', '0 9 * * *', 'list', or 'cancel <id>')", type: 3, required: false },
|
|
682
|
+
{ name: "prompt", description: "Prompt or command to execute when triggered", type: 3, required: false }
|
|
683
|
+
]
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: "bind",
|
|
687
|
+
description: "Bind the current Discord thread to a specific local workspace directory",
|
|
688
|
+
options: [
|
|
689
|
+
{ name: "workspace", description: "Path to workspace directory (or 'unbind' to clear)", type: 3, required: true }
|
|
690
|
+
]
|
|
691
|
+
},
|
|
692
|
+
{ name: "threads", description: "List all Discord threads bound to isolated workspaces and sessions" },
|
|
693
|
+
{ name: "stop", description: "Cancel running agent or command" },
|
|
694
|
+
{ name: "status", description: "View daemon status and provider connection" },
|
|
695
|
+
{ name: "help", description: "Display Dis-Gravity commands" }
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
if (config.guildId) {
|
|
699
|
+
const guild = await client.guilds.fetch(config.guildId).catch(() => null);
|
|
700
|
+
if (guild) {
|
|
701
|
+
await guild.commands.set(slashCommands);
|
|
702
|
+
log.appendLine(`Registered ${slashCommands.length} guild slash commands in ${guild.name}`);
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
await client.application.commands.set(slashCommands);
|
|
706
|
+
log.appendLine(`Registered ${slashCommands.length} global slash commands`);
|
|
707
|
+
}
|
|
708
|
+
} catch (err) {
|
|
709
|
+
log.appendLine(`Notice: Could not register slash commands (${err.message}). Prefix commands (!sh, !diff, etc.) are fully active.`);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Handle Slash Commands and Buttons
|
|
714
|
+
client.on('interactionCreate', async (interaction) => {
|
|
715
|
+
try {
|
|
716
|
+
if (!isUserAllowed(interaction.user.id)) {
|
|
717
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
718
|
+
const hasWhitelist = config.allowedUsers && config.allowedUsers.length > 0;
|
|
719
|
+
const denial = hasWhitelist
|
|
720
|
+
? `⛔ **Unauthorized:** Your Discord User ID (\`${interaction.user.id}\`) is not on the Dis-Gravity whitelist.`
|
|
721
|
+
: `⛔ **Unauthorized:** No \`allowedUsers\` whitelist is configured in Dis-Gravity daemon. For security, all remote interactions are blocked by default. Add your Discord User ID (\`${interaction.user.id}\`) via \`--allowed-users\` or in \`dis-gravity.config.json\`.`;
|
|
722
|
+
await interaction.reply({ content: denial, ephemeral: true });
|
|
723
|
+
}
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const activeWs = resolveThreadWorkspace(interaction.channel);
|
|
728
|
+
|
|
729
|
+
if (interaction.isButton()) {
|
|
730
|
+
const id = interaction.customId;
|
|
731
|
+
if (id === 'daemon:status') {
|
|
732
|
+
await interaction.deferUpdate().catch(() => { });
|
|
733
|
+
const statusPayload = await getDaemonStatusPayload(null, activeWs);
|
|
734
|
+
await interaction.editReply(statusPayload).catch(() => { });
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (id === 'daemon:diff') {
|
|
738
|
+
await interaction.deferReply({ ephemeral: true }).catch(() => { });
|
|
739
|
+
const st = await executeShellCommand('git status --short', activeWs);
|
|
740
|
+
const df = await executeShellCommand('git diff --stat', activeWs);
|
|
741
|
+
let reply = `🌿 **Git Status & Diff Preview** (\`${path.basename(activeWs)}\`)\n\n\`\`\`diff\n${st.stdout || 'Clean working tree'}\n\`\`\`\n`;
|
|
742
|
+
if (df.stdout) reply += `**Diff Stat:**\n\`\`\`text\n${df.stdout}\n\`\`\``;
|
|
743
|
+
await interaction.editReply(reply).catch(() => { });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (id === 'daemon:info') {
|
|
747
|
+
await interaction.deferReply({ ephemeral: true }).catch(() => { });
|
|
748
|
+
const cpus = os.cpus();
|
|
749
|
+
const cpuModel = cpus && cpus[0] ? cpus[0].model.trim() : 'CPU';
|
|
750
|
+
const freeGB = (os.freemem() / (1024 ** 3)).toFixed(1);
|
|
751
|
+
const totalGB = (os.totalmem() / (1024 ** 3)).toFixed(1);
|
|
752
|
+
const st = await executeShellCommand('git rev-parse --abbrev-ref HEAD', activeWs);
|
|
753
|
+
let msg = `🖥️ **Host & System Info**\n\n`;
|
|
754
|
+
msg += `• **Project:** \`${path.basename(activeWs)}\`\n`;
|
|
755
|
+
msg += `• **Workspace:** \`${activeWs}\`\n`;
|
|
756
|
+
msg += `• **Branch:** \`${st.stdout.trim() || 'unknown'}\`\n`;
|
|
757
|
+
msg += `• **System:** ${os.platform()} (${os.release()}, ${os.arch()})\n`;
|
|
758
|
+
msg += `• **CPU:** ${cpuModel} (${cpus ? cpus.length : 1} cores)\n`;
|
|
759
|
+
msg += `• **Memory:** ${freeGB} GB free / ${totalGB} GB total\n`;
|
|
760
|
+
await interaction.editReply(msg).catch(() => { });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (id === 'schedule:cancel_all') {
|
|
764
|
+
const count = scheduler.cancelAll();
|
|
765
|
+
await interaction.update({ content: `🧹 **Cancelled all ${count} scheduled task(s).**`, components: [] }).catch(() => { });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (id.startsWith('schedule:cancel:')) {
|
|
769
|
+
const jobId = id.replace('schedule:cancel:', '');
|
|
770
|
+
scheduler.cancelJob(jobId);
|
|
771
|
+
const updated = buildScheduleListPayload();
|
|
772
|
+
await interaction.update(updated).catch(() => { });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (id.startsWith('approve_') && approvalResolver) {
|
|
776
|
+
const choice = id.replace('approve_', '');
|
|
777
|
+
approvalResolver(choice);
|
|
778
|
+
approvalResolver = null;
|
|
779
|
+
pendingApproval = null;
|
|
780
|
+
await interaction.reply({ content: `✅ **Action approved:** \`${choice}\``, ephemeral: true }).catch(() => { });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (id === 'agent_cancel') {
|
|
784
|
+
await cancelActiveAgent();
|
|
785
|
+
await interaction.reply({ content: "🛑 **Execution halted.**", ephemeral: true }).catch(() => { });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (interaction.isChatInputCommand()) {
|
|
791
|
+
const cmd = interaction.commandName;
|
|
792
|
+
if (cmd === 'sh') {
|
|
793
|
+
const commandStr = interaction.options.getString('command');
|
|
794
|
+
await interaction.deferReply();
|
|
795
|
+
const res = await executeShellCommand(commandStr, activeWs);
|
|
796
|
+
const badge = res.code === 0 ? `✅ **Exit 0 (${res.duration}s)** | 📁 \`${path.basename(activeWs)}\`` : `❌ **Exit ${res.code} (${res.duration}s)** | 📁 \`${path.basename(activeWs)}\``;
|
|
797
|
+
const out = res.stdout || res.stderr || '(No output)';
|
|
798
|
+
await interaction.editReply(`${badge}\n\`\`\`text\n${out.substring(0, 1800)}\n\`\`\``);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (cmd === 'diff') {
|
|
803
|
+
await interaction.deferReply();
|
|
804
|
+
const st = await executeShellCommand('git status --short', activeWs);
|
|
805
|
+
const df = await executeShellCommand('git diff --stat', activeWs);
|
|
806
|
+
let reply = `🌿 **Git Status & Diff Preview** (\`${path.basename(activeWs)}\`)\n\n`;
|
|
807
|
+
reply += `\`\`\`diff\n${st.stdout || 'Clean working tree'}\n\`\`\`\n`;
|
|
808
|
+
if (df.stdout) reply += `**Diff Stat:**\n\`\`\`text\n${df.stdout}\n\`\`\``;
|
|
809
|
+
await interaction.editReply(reply);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (cmd === 'tree') {
|
|
814
|
+
await interaction.deferReply();
|
|
815
|
+
const tree = listWorkspaceTree(activeWs, '.', 2, 80);
|
|
816
|
+
await interaction.editReply(`📁 **Workspace Tree (\`${path.basename(activeWs)}\`):**\n\`\`\`text\n${tree.substring(0, 1800)}\n\`\`\``);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (cmd === 'schedule') {
|
|
821
|
+
const timeOpt = (interaction.options.getString('time') || '').trim();
|
|
822
|
+
const promptOpt = (interaction.options.getString('prompt') || '').trim();
|
|
823
|
+
if (!timeOpt || /^list$/i.test(timeOpt)) {
|
|
824
|
+
await interaction.reply(buildScheduleListPayload());
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (/^cancel/i.test(timeOpt)) {
|
|
828
|
+
const target = timeOpt.replace(/^cancel\s*/i, '').trim() || promptOpt;
|
|
829
|
+
if (target === 'all') {
|
|
830
|
+
const count = scheduler.cancelAll();
|
|
831
|
+
await interaction.reply(`🧹 **Cancelled all ${count} scheduled task(s).**`);
|
|
832
|
+
} else {
|
|
833
|
+
const ok = scheduler.cancelJob(target);
|
|
834
|
+
await interaction.reply(ok ? `🗑️ **Cancelled scheduled task #${target}.**` : `⚠️ **Schedule #${target} not found.**`);
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (!promptOpt) {
|
|
839
|
+
const parsedCmd = parseScheduleCommandInput(timeOpt);
|
|
840
|
+
if (parsedCmd.action === 'add') {
|
|
841
|
+
const job = scheduler.addJob({
|
|
842
|
+
spec: parsedCmd.parsedSpec,
|
|
843
|
+
prompt: parsedCmd.prompt,
|
|
844
|
+
threadId: interaction.channel?.isThread?.() ? interaction.channelId : null,
|
|
845
|
+
workspace: activeWs,
|
|
846
|
+
author: interaction.user.username,
|
|
847
|
+
onTrigger: triggerScheduledJob
|
|
848
|
+
});
|
|
849
|
+
await interaction.reply(`⏰ **Scheduled Task #${job.id}** (\`${job.spec}\` — ${job.description})\n• **Prompt:** \`${job.prompt}\`\n• **Workspace:** \`${activeWs}\`\n• **Next Run:** ${new Date(job.nextRunAt).toLocaleString()}`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
await interaction.reply({ content: `⚠️ Please specify both \`time\` and \`prompt\`, e.g. \`/schedule time:30m prompt:run npm test\`.`, ephemeral: true });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
const job = scheduler.addJob({
|
|
857
|
+
spec: timeOpt,
|
|
858
|
+
prompt: promptOpt,
|
|
859
|
+
threadId: interaction.channel?.isThread?.() ? interaction.channelId : null,
|
|
860
|
+
workspace: activeWs,
|
|
861
|
+
author: interaction.user.username,
|
|
862
|
+
onTrigger: triggerScheduledJob
|
|
863
|
+
});
|
|
864
|
+
await interaction.reply(`⏰ **Scheduled Task #${job.id}** (\`${job.spec}\` — ${job.description})\n• **Prompt:** \`${job.prompt}\`\n• **Workspace:** \`${activeWs}\`\n• **Next Run:** ${new Date(job.nextRunAt).toLocaleString()}`);
|
|
865
|
+
} catch (err) {
|
|
866
|
+
await interaction.reply({ content: `❌ **Schedule Error:** ${err.message}`, ephemeral: true });
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (cmd === 'bind') {
|
|
872
|
+
const wsArg = (interaction.options.getString('workspace') || '').trim();
|
|
873
|
+
if (!interaction.channel?.isThread?.()) {
|
|
874
|
+
await interaction.reply({ content: `⚠️ \`/bind\` must be run inside a Discord thread, or use \`!thread <name> <workspace_path>\` in the main channel to create a bound workspace thread.`, ephemeral: true });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (/^(unbind|clear|none)$/i.test(wsArg)) {
|
|
878
|
+
threadWorkspaces.unbindThread(interaction.channelId);
|
|
879
|
+
await interaction.reply(`🔓 **Unbound thread from custom workspace.** Reverting to default workspace (\`${config.workspace}\`).`);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
const binding = threadWorkspaces.bindThread(interaction.channelId, wsArg, {
|
|
884
|
+
boundBy: interaction.user.username,
|
|
885
|
+
baseWorkspace: path.dirname(config.workspace)
|
|
886
|
+
});
|
|
887
|
+
await interaction.reply(`🧵 **Thread Bound to Workspace:** 📁 **\`${binding.label}\`**\n> Path: \`${binding.workspace}\`\n\nAll commands (\`/sh\`, \`/diff\`, \`/tree\`, prompts) in this thread now execute isolated in this workspace.`);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
await interaction.reply({ content: `❌ **Failed to bind workspace:** ${err.message}`, ephemeral: true });
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (cmd === 'threads') {
|
|
895
|
+
const list = threadWorkspaces.listBindings();
|
|
896
|
+
if (list.length === 0) {
|
|
897
|
+
await interaction.reply(`🧵 **Bound Workspace Threads (0 active)**\n\nNo threads are currently bound to custom workspaces.\n💡 Use \`/bind <workspace_path>\` inside any thread or \`!thread <name> <workspace_path>\` to create one.`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
let msg = `🧵 **Active Workspace Thread Bindings (${list.length}):**\n\n`;
|
|
901
|
+
for (const b of list) {
|
|
902
|
+
msg += `• <#${b.threadId}> → 📁 **\`${b.label}\`** (\`${b.workspace}\`)${b.conversationName ? ` • 💬 *${b.conversationName}*` : ''}\n`;
|
|
903
|
+
}
|
|
904
|
+
await interaction.reply(msg.substring(0, 1900));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (cmd === 'info') {
|
|
909
|
+
await interaction.deferReply();
|
|
910
|
+
const cpus = os.cpus();
|
|
911
|
+
const cpuModel = cpus && cpus[0] ? cpus[0].model.trim() : 'CPU';
|
|
912
|
+
const freeGB = (os.freemem() / (1024 ** 3)).toFixed(1);
|
|
913
|
+
const totalGB = (os.totalmem() / (1024 ** 3)).toFixed(1);
|
|
914
|
+
const st = await executeShellCommand('git rev-parse --abbrev-ref HEAD', activeWs);
|
|
915
|
+
let msg = `🖥️ **Dis-Gravity Host & Workspace Health Dashboard**\n\n`;
|
|
916
|
+
msg += `• **Project:** \`${path.basename(activeWs)}\`\n`;
|
|
917
|
+
msg += `• **Path:** \`${activeWs}\`\n`;
|
|
918
|
+
msg += `• **Git Branch:** \`${st.stdout.trim() || 'unknown'}\`\n`;
|
|
919
|
+
msg += `• **Provider:** 🟢 **Standalone Daemon (${config.provider.toUpperCase()})**\n`;
|
|
920
|
+
msg += `• **System:** ${os.platform()} (${os.release()}, ${os.arch()})\n`;
|
|
921
|
+
msg += `• **CPU:** ${cpuModel} (${cpus ? cpus.length : 1} cores)\n`;
|
|
922
|
+
msg += `• **Memory:** ${freeGB} GB free / ${totalGB} GB total\n`;
|
|
923
|
+
msg += `• **Node Runtime:** ${process.version} (PID: ${process.pid})\n`;
|
|
924
|
+
await interaction.editReply(msg);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (cmd === 'status') {
|
|
929
|
+
await interaction.deferReply().catch(() => { });
|
|
930
|
+
const statusPayload = await getDaemonStatusPayload(null, activeWs);
|
|
931
|
+
await interaction.editReply(statusPayload).catch(() => { });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (cmd === 'stop') {
|
|
936
|
+
await cancelActiveAgent();
|
|
937
|
+
await interaction.reply('🛑 **Agent generation / command cancelled.**');
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (cmd === 'help') {
|
|
942
|
+
let h = `🤖 **Dis-Gravity Standalone Daemon Commands**\n\n`;
|
|
943
|
+
h += `• \`/sh <cmd>\` or \`!sh <cmd>\` — Run terminal command\n`;
|
|
944
|
+
h += `• \`/diff\` or \`!diff\` — View git status and diff preview\n`;
|
|
945
|
+
h += `• \`/schedule <time> <prompt>\` or \`!schedule\` — Schedule delayed or cron prompts\n`;
|
|
946
|
+
h += `• \`/bind <workspace>\` or \`!bind <path>\` — Bind current thread to a workspace folder\n`;
|
|
947
|
+
h += `• \`/threads\` or \`!threads\` — List bound workspace threads\n`;
|
|
948
|
+
h += `• 🎙️ **Voice Notes** — Send a Discord voice message to auto-transcribe & run\n`;
|
|
949
|
+
h += `• \`/info\` or \`!info\` — System hardware and workspace dashboard\n`;
|
|
950
|
+
h += `• \`/stop\` or \`!stop\` — Cancel running process\n`;
|
|
951
|
+
await interaction.reply(h);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} catch (err) {
|
|
956
|
+
log.appendLine(`Interaction error: ${err.message}`);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Handle Chat Messages
|
|
961
|
+
client.on('messageCreate', async (message) => {
|
|
962
|
+
if (message.author.bot) return;
|
|
963
|
+
const isMainChannel = message.channelId === config.channelId;
|
|
964
|
+
const isBoundThread = message.channel?.isThread?.() && message.channel.parentId === config.channelId;
|
|
965
|
+
if (!isMainChannel && !isBoundThread && !message.channel.isDMBased()) return;
|
|
966
|
+
|
|
967
|
+
if (!isUserAllowed(message.author.id)) {
|
|
968
|
+
const hasWhitelist = config.allowedUsers && config.allowedUsers.length > 0;
|
|
969
|
+
const denial = hasWhitelist
|
|
970
|
+
? `⛔ **Unauthorized:** Your Discord User ID (\`${message.author.id}\`) is not in the Dis-Gravity whitelist.`
|
|
971
|
+
: `⛔ **Unauthorized:** No \`allowedUsers\` whitelist is configured in Dis-Gravity daemon. For security, all remote commands and prompts are blocked by default. Add your Discord User ID (\`${message.author.id}\`) via \`--allowed-users\` or in \`dis-gravity.config.json\`.`;
|
|
972
|
+
await message.reply(denial).catch(() => { });
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const activeWs = resolveThreadWorkspace(message.channel);
|
|
977
|
+
let text = message.content.trim();
|
|
978
|
+
|
|
979
|
+
// Voice Message / Audio Attachment Transcription
|
|
980
|
+
if (message.attachments && message.attachments.size > 0) {
|
|
981
|
+
const attachmentsList = Array.from(message.attachments.values());
|
|
982
|
+
const audioAtt = attachmentsList.find(a => isAudioAttachment(a, message.flags?.bitfield || 0));
|
|
983
|
+
if (audioAtt && audioAtt.url) {
|
|
984
|
+
try {
|
|
985
|
+
const parsedUrl = new URL(audioAtt.url);
|
|
986
|
+
if (parsedUrl.protocol === 'https:' && ['cdn.discordapp.com', 'media.discordapp.net'].includes(parsedUrl.hostname.toLowerCase())) {
|
|
987
|
+
const tmpDir = path.join(activeWs, '.disgravity', 'attachments');
|
|
988
|
+
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
989
|
+
const ext = path.extname(audioAtt.name || 'voice.ogg') || '.ogg';
|
|
990
|
+
const localAudioPath = path.join(tmpDir, `voice_${Date.now()}${ext}`);
|
|
991
|
+
const res = await fetch(audioAtt.url);
|
|
992
|
+
if (res.ok) {
|
|
993
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
994
|
+
fs.writeFileSync(localAudioPath, buf);
|
|
995
|
+
const stt = await transcribeAudioFile(localAudioPath);
|
|
996
|
+
if (stt.ok && stt.text) {
|
|
997
|
+
await message.reply(`🎙️ **Voice Prompt Transcribed (${stt.provider}):**\n> *"${stt.text}"*`).catch(() => { });
|
|
998
|
+
text = text ? `${text}\n${stt.text}` : stt.text;
|
|
999
|
+
} else if (!text) {
|
|
1000
|
+
await message.reply(`🎙️ **Voice message saved** (\`${path.basename(localAudioPath)}\`), but automatic transcription is not configured (${stt.error}). Forwarding audio file path to agent.`).catch(() => { });
|
|
1001
|
+
text = `Please process the attached audio recording at ${localAudioPath}`;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
log.appendLine(`[Voice] Error processing audio attachment: ${err.message}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (!text) return;
|
|
1012
|
+
|
|
1013
|
+
// Schedule command (!schedule or /schedule)
|
|
1014
|
+
if (text === '!schedule' || text.startsWith('!schedule ') || text === '/schedule' || text.startsWith('/schedule ')) {
|
|
1015
|
+
const rawArg = text.replace(/^[/!]schedule\s*/i, '').trim();
|
|
1016
|
+
const parsedCmd = parseScheduleCommandInput(rawArg);
|
|
1017
|
+
if (parsedCmd.action === 'list') {
|
|
1018
|
+
await message.reply(buildScheduleListPayload()).catch(() => { });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (parsedCmd.action === 'cancel') {
|
|
1022
|
+
if (parsedCmd.target === 'all') {
|
|
1023
|
+
const count = scheduler.cancelAll();
|
|
1024
|
+
await message.reply(`🧹 **Cancelled all ${count} scheduled task(s).**`);
|
|
1025
|
+
} else {
|
|
1026
|
+
const ok = scheduler.cancelJob(parsedCmd.target);
|
|
1027
|
+
await message.reply(ok ? `🗑️ **Cancelled scheduled task #${parsedCmd.target}.**` : `⚠️ **Schedule #${parsedCmd.target} not found.**`);
|
|
1028
|
+
}
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (parsedCmd.action === 'add') {
|
|
1032
|
+
try {
|
|
1033
|
+
const job = scheduler.addJob({
|
|
1034
|
+
spec: parsedCmd.parsedSpec,
|
|
1035
|
+
prompt: parsedCmd.prompt,
|
|
1036
|
+
threadId: isBoundThread ? message.channelId : null,
|
|
1037
|
+
workspace: activeWs,
|
|
1038
|
+
author: message.author.username,
|
|
1039
|
+
onTrigger: triggerScheduledJob
|
|
1040
|
+
});
|
|
1041
|
+
await message.reply(`⏰ **Scheduled Task #${job.id}** (\`${job.spec}\` — ${job.description})\n• **Prompt:** \`${job.prompt}\`\n• **Workspace:** \`${activeWs}\`\n• **Next Run:** ${new Date(job.nextRunAt).toLocaleString()}`);
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
await message.reply(`❌ **Schedule Error:** ${err.message}`);
|
|
1044
|
+
}
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
await message.reply(`⚠️ ${parsedCmd.error}`);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Bind thread to workspace (!bind <path> or /bind <path>)
|
|
1052
|
+
if (text.startsWith('!bind ') || text.startsWith('/bind ')) {
|
|
1053
|
+
const wsArg = text.replace(/^[/!]bind\s+/i, '').trim();
|
|
1054
|
+
if (!isBoundThread) {
|
|
1055
|
+
await message.reply(`⚠️ Use \`!bind <workspace_path>\` inside a Discord thread, or use \`!thread <name> <workspace_path>\` here to create a new workspace thread.`);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (/^(unbind|clear|none)$/i.test(wsArg)) {
|
|
1059
|
+
threadWorkspaces.unbindThread(message.channelId);
|
|
1060
|
+
await message.reply(`🔓 **Unbound thread from custom workspace.** Reverting to \`${config.workspace}\`.`);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
const binding = threadWorkspaces.bindThread(message.channelId, wsArg, {
|
|
1065
|
+
boundBy: message.author.username,
|
|
1066
|
+
baseWorkspace: path.dirname(config.workspace)
|
|
1067
|
+
});
|
|
1068
|
+
await message.reply(`🧵 **Thread Bound to Workspace:** 📁 **\`${binding.label}\`**\n> Path: \`${binding.workspace}\``);
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
await message.reply(`❌ **Failed to bind workspace:** ${err.message}`);
|
|
1071
|
+
}
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Create bound workspace thread (!thread <name> [workspace_path])
|
|
1076
|
+
if (text.startsWith('!thread ') || text.startsWith('/thread ')) {
|
|
1077
|
+
const rawArgs = text.replace(/^[/!]thread\s+/i, '').trim();
|
|
1078
|
+
const parts = rawArgs.split(/\s+/);
|
|
1079
|
+
const threadName = parts[0] || 'session';
|
|
1080
|
+
const wsArg = parts.slice(1).join(' ') || activeWs;
|
|
1081
|
+
try {
|
|
1082
|
+
const thread = await message.startThread({
|
|
1083
|
+
name: `📁 ${threadName}`.substring(0, 100),
|
|
1084
|
+
autoArchiveDuration: 1440
|
|
1085
|
+
});
|
|
1086
|
+
const binding = threadWorkspaces.bindThread(thread.id, wsArg, {
|
|
1087
|
+
label: threadName,
|
|
1088
|
+
boundBy: message.author.username,
|
|
1089
|
+
baseWorkspace: path.dirname(config.workspace)
|
|
1090
|
+
});
|
|
1091
|
+
await thread.send(`🧵 **Dedicated Workspace Thread Ready**\n• **Session:** \`${threadName}\`\n• **Bound Workspace:** \`${binding.workspace}\`\n\nAll commands and AI prompts in this thread will run isolated in \`${binding.workspace}\`.`);
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
await message.reply(`❌ **Could not create workspace thread:** ${err.message}`);
|
|
1094
|
+
}
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// List bound threads (!threads or /threads)
|
|
1099
|
+
if (text === '!threads' || text === '/threads') {
|
|
1100
|
+
const list = threadWorkspaces.listBindings();
|
|
1101
|
+
if (list.length === 0) {
|
|
1102
|
+
await message.reply(`🧵 **Bound Workspace Threads (0 active)**\nUse \`!bind <workspace_path>\` inside a thread or \`!thread <name> <workspace_path>\` to create one.`);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
let msg = `🧵 **Active Workspace Thread Bindings (${list.length}):**\n\n`;
|
|
1106
|
+
for (const b of list) {
|
|
1107
|
+
msg += `• <#${b.threadId}> → 📁 **\`${b.label}\`** (\`${b.workspace}\`)\n`;
|
|
1108
|
+
}
|
|
1109
|
+
await message.reply(msg.substring(0, 1900));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Shell command prefix
|
|
1114
|
+
if (text.startsWith('!sh ') || text.startsWith('$ ')) {
|
|
1115
|
+
const cmdStr = text.replace(/^(!sh |\$ )/, '');
|
|
1116
|
+
const statusMsg = await message.reply(`⚡ Running: \`${cmdStr}\` in \`${path.basename(activeWs)}\`...`);
|
|
1117
|
+
const res = await executeShellCommand(cmdStr, activeWs);
|
|
1118
|
+
const badge = res.code === 0 ? `✅ **Exit 0 (${res.duration}s)** | 📁 \`${path.basename(activeWs)}\`` : `❌ **Exit ${res.code} (${res.duration}s)** | 📁 \`${path.basename(activeWs)}\``;
|
|
1119
|
+
const out = res.stdout || res.stderr || '(No output)';
|
|
1120
|
+
await statusMsg.edit(`${badge}\n\`\`\`text\n${out.substring(0, 1800)}\n\`\`\``);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Status
|
|
1125
|
+
if (text === '!status' || text === '/status') {
|
|
1126
|
+
const statusPayload = await getDaemonStatusPayload(null, activeWs);
|
|
1127
|
+
await message.reply(statusPayload).catch(() => { });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Git Diff
|
|
1132
|
+
if (text === '!diff' || text === '/diff') {
|
|
1133
|
+
const st = await executeShellCommand('git status --short', activeWs);
|
|
1134
|
+
const df = await executeShellCommand('git diff --stat', activeWs);
|
|
1135
|
+
let reply = `🌿 **Git Status & Diff Preview** (\`${path.basename(activeWs)}\`)\n\n\`\`\`diff\n${st.stdout || 'Clean working tree'}\n\`\`\`\n`;
|
|
1136
|
+
if (df.stdout) reply += `**Diff Stat:**\n\`\`\`text\n${df.stdout}\n\`\`\``;
|
|
1137
|
+
await message.reply(reply);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Info
|
|
1142
|
+
if (text === '!info' || text === '/info') {
|
|
1143
|
+
const cpus = os.cpus();
|
|
1144
|
+
const cpuModel = cpus && cpus[0] ? cpus[0].model.trim() : 'CPU';
|
|
1145
|
+
const freeGB = (os.freemem() / (1024 ** 3)).toFixed(1);
|
|
1146
|
+
const totalGB = (os.totalmem() / (1024 ** 3)).toFixed(1);
|
|
1147
|
+
const st = await executeShellCommand('git rev-parse --abbrev-ref HEAD', activeWs);
|
|
1148
|
+
let msg = `🖥️ **Dis-Gravity Host & Workspace Health Dashboard**\n\n`;
|
|
1149
|
+
msg += `• **Project:** \`${path.basename(activeWs)}\`\n`;
|
|
1150
|
+
msg += `• **Path:** \`${activeWs}\`\n`;
|
|
1151
|
+
msg += `• **Git Branch:** \`${st.stdout.trim() || 'unknown'}\`\n`;
|
|
1152
|
+
msg += `• **Provider:** 🟢 **Standalone Daemon (${config.provider.toUpperCase()})**\n`;
|
|
1153
|
+
msg += `• **System:** ${os.platform()} (${os.release()}, ${os.arch()})\n`;
|
|
1154
|
+
msg += `• **CPU:** ${cpuModel} (${cpus ? cpus.length : 1} cores)\n`;
|
|
1155
|
+
msg += `• **Memory:** ${freeGB} GB free / ${totalGB} GB total\n`;
|
|
1156
|
+
await message.reply(msg);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Stop
|
|
1161
|
+
if (text === '!stop' || text === '/stop') {
|
|
1162
|
+
await cancelActiveAgent();
|
|
1163
|
+
await message.reply('🛑 **Agent generation / command cancelled.**');
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Forward general prompt to AI Agent
|
|
1168
|
+
const statusMsg = await message.reply('⏳ **Analyzing prompt... (0s)**');
|
|
1169
|
+
let elapsed = 0;
|
|
1170
|
+
const timer = setInterval(async () => {
|
|
1171
|
+
elapsed += 2;
|
|
1172
|
+
const statusText = activeStepStatus ? `⏳ **Working (${elapsed}s):** \`${activeStepStatus}\`` : `⏳ **Analyzing... (${elapsed}s)**`;
|
|
1173
|
+
await statusMsg.edit(statusText).catch(() => { });
|
|
1174
|
+
}, 2000);
|
|
1175
|
+
|
|
1176
|
+
try {
|
|
1177
|
+
const res = await executeCliAgent(text, (step) => {
|
|
1178
|
+
statusMsg.edit(`⏳ **Working (${elapsed}s):** \`${step}\``).catch(() => { });
|
|
1179
|
+
}, activeWs);
|
|
1180
|
+
|
|
1181
|
+
clearInterval(timer);
|
|
1182
|
+
await statusMsg.edit(res);
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
clearInterval(timer);
|
|
1185
|
+
await statusMsg.edit(`❌ **Error:** ${err.message}`);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
await client.login(config.token);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (require.main === module) {
|
|
1193
|
+
startDaemon().catch((err) => {
|
|
1194
|
+
console.error('Fatal Daemon Error:', err);
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
module.exports = { startDaemon };
|