@hailer/mcp 0.2.3 → 0.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.
@@ -39,7 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.bugFixerTools = exports.bugFixerPublishAppTool = exports.bugFixerGitRevertTool = exports.bugFixerGitPushTool = exports.bugFixerGitCommitTool = exports.bugFixerGitPullTool = exports.bugFixerGitStatusTool = exports.bugFixerRunBuildTool = exports.bugFixerApplyFixTool = exports.bugFixerWriteFileTool = exports.bugFixerReadFileTool = exports.bugFixerListFilesTool = exports.bugFixerFindAppTool = void 0;
42
+ exports.bugFixerTools = exports.bugFixerRetryFixTool = exports.bugFixerPublishFixTool = exports.bugFixerMarkDeclinedTool = exports.bugFixerStartFixTool = exports.bugFixerAnalyzeBugTool = exports.bugFixerPublishAppTool = exports.bugFixerGitRevertTool = exports.bugFixerGitPushTool = exports.bugFixerGitCommitTool = exports.bugFixerGitPullTool = exports.bugFixerGitStatusTool = exports.bugFixerRunBuildTool = exports.bugFixerApplyFixTool = exports.bugFixerWriteFileTool = exports.bugFixerReadFileTool = exports.bugFixerListFilesTool = exports.bugFixerFindAppTool = void 0;
43
43
  const zod_1 = require("zod");
44
44
  const child_process_1 = require("child_process");
45
45
  const fs = __importStar(require("fs/promises"));
@@ -47,36 +47,76 @@ const path = __importStar(require("path"));
47
47
  const tool_registry_1 = require("../tool-registry");
48
48
  const logger_1 = require("../../lib/logger");
49
49
  const config_1 = require("../../config");
50
+ const pending_classification_1 = require("../../agents/bug-fixer/registries/pending-classification");
51
+ const pending_fix_1 = require("../../agents/bug-fixer/registries/pending-fix");
50
52
  const logger = (0, logger_1.createLogger)({ component: "bug-fixer-tools" });
51
- const appsBasePath = config_1.environment.DEV_APPS_PATH || process.cwd();
52
- // Helper to scan for apps
53
+ // Parse all configured app paths
54
+ function getAppPaths() {
55
+ const paths = [];
56
+ // Multiple paths (comma-separated)
57
+ if (config_1.environment.DEV_APPS_PATHS) {
58
+ paths.push(...config_1.environment.DEV_APPS_PATHS.split(',').map(p => p.trim()).filter(p => p.length > 0));
59
+ }
60
+ // Single path (fallback)
61
+ if (config_1.environment.DEV_APPS_PATH && !paths.includes(config_1.environment.DEV_APPS_PATH)) {
62
+ paths.push(config_1.environment.DEV_APPS_PATH);
63
+ }
64
+ // Default to cwd if nothing configured
65
+ if (paths.length === 0) {
66
+ paths.push(process.cwd());
67
+ }
68
+ return paths;
69
+ }
70
+ // Cache for scanned apps
71
+ let appsCache = null;
72
+ // Helper to scan for apps across all configured directories
53
73
  async function scanApps() {
74
+ // Return cached if available
75
+ if (appsCache)
76
+ return appsCache;
54
77
  const apps = [];
55
- try {
56
- const entries = await fs.readdir(appsBasePath, { withFileTypes: true });
57
- for (const entry of entries) {
58
- if (entry.isDirectory() && !entry.name.startsWith(".") && !["node_modules", "dist"].includes(entry.name)) {
59
- const appPath = path.join(appsBasePath, entry.name);
60
- // Check for manifest.json (Hailer app indicator)
61
- try {
62
- await fs.access(path.join(appPath, "manifest.json"));
63
- apps.push({ name: entry.name, path: appPath });
64
- }
65
- catch {
78
+ const appPaths = getAppPaths();
79
+ for (const basePath of appPaths) {
80
+ try {
81
+ const entries = await fs.readdir(basePath, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ if (entry.isDirectory() && !entry.name.startsWith(".") && !["node_modules", "dist"].includes(entry.name)) {
84
+ const appPath = path.join(basePath, entry.name);
85
+ // Try to read manifest for appId
86
+ let manifest = null;
66
87
  try {
67
- await fs.access(path.join(appPath, "public", "manifest.json"));
68
- apps.push({ name: entry.name, path: appPath });
88
+ const content = await fs.readFile(path.join(appPath, "manifest.json"), "utf-8");
89
+ manifest = JSON.parse(content);
69
90
  }
70
91
  catch {
71
- // Not a Hailer app
92
+ try {
93
+ const content = await fs.readFile(path.join(appPath, "public", "manifest.json"), "utf-8");
94
+ manifest = JSON.parse(content);
95
+ }
96
+ catch {
97
+ // Not a Hailer app
98
+ }
99
+ }
100
+ if (manifest) {
101
+ apps.push({
102
+ name: manifest.name || entry.name,
103
+ path: appPath,
104
+ appId: manifest.appId
105
+ });
72
106
  }
73
107
  }
74
108
  }
75
109
  }
110
+ catch (e) {
111
+ logger.warn("Failed to scan apps directory", { path: basePath, error: e });
112
+ }
76
113
  }
77
- catch (e) {
78
- logger.warn("Failed to scan apps", { error: e });
79
- }
114
+ // Cache results
115
+ appsCache = apps;
116
+ logger.info("Scanned apps for bug fixer tools", {
117
+ paths: appPaths,
118
+ found: apps.map(a => ({ name: a.name, appId: a.appId?.substring(0, 8) || "none" }))
119
+ });
80
120
  return apps;
81
121
  }
82
122
  // Find App Tool
@@ -86,13 +126,14 @@ exports.bugFixerFindAppTool = {
86
126
  description: "Find a Hailer app project by name or from bug title",
87
127
  schema: zod_1.z.object({
88
128
  searchTerm: zod_1.z.string().describe("App name or bug title to search for").optional(),
89
- name: zod_1.z.string().describe("Alias for searchTerm - app name to search for").optional()
90
- }).refine(data => data.searchTerm || data.name, {
91
- message: "Either searchTerm or name is required"
129
+ name: zod_1.z.string().describe("Alias for searchTerm - app name to search for").optional(),
130
+ appName: zod_1.z.string().describe("Alias for searchTerm - app name to search for").optional()
131
+ }).refine(data => data.searchTerm || data.name || data.appName, {
132
+ message: "Either searchTerm, name, or appName is required"
92
133
  }),
93
134
  async execute(args, _context) {
94
- // Accept either searchTerm or name
95
- const searchTerm = (args.searchTerm || args.name || '').toLowerCase();
135
+ // Accept searchTerm, name, or appName
136
+ const searchTerm = (args.searchTerm || args.name || args.appName || '').toLowerCase();
96
137
  const apps = await scanApps();
97
138
  for (const app of apps) {
98
139
  if (app.name.toLowerCase().includes(searchTerm) ||
@@ -113,9 +154,14 @@ exports.bugFixerListFilesTool = {
113
154
  group: tool_registry_1.ToolGroup.BOT_INTERNAL,
114
155
  description: "List source files in a Hailer app project",
115
156
  schema: zod_1.z.object({
116
- appPath: zod_1.z.string().describe("Path to the app project")
157
+ appPath: zod_1.z.string().describe("Path to the app project").optional(),
158
+ app: zod_1.z.string().describe("Alias for appPath - path to the app project").optional()
159
+ }).refine(data => data.appPath || data.app, {
160
+ message: "Either appPath or app is required"
117
161
  }),
118
162
  async execute(args, _context) {
163
+ // Accept either appPath or app
164
+ const appPath = args.appPath || args.app || '';
119
165
  const files = [];
120
166
  const extensions = [".tsx", ".ts", ".jsx", ".js"];
121
167
  const ignoreDirs = ["node_modules", "dist", "build", ".git"];
@@ -126,7 +172,7 @@ exports.bugFixerListFilesTool = {
126
172
  const entries = await fs.readdir(dir, { withFileTypes: true });
127
173
  for (const entry of entries) {
128
174
  const fullPath = path.join(dir, entry.name);
129
- const relativePath = path.relative(args.appPath, fullPath);
175
+ const relativePath = path.relative(appPath, fullPath);
130
176
  if (entry.isDirectory()) {
131
177
  if (!ignoreDirs.includes(entry.name) && !entry.name.startsWith(".")) {
132
178
  await scan(fullPath, depth + 1);
@@ -141,7 +187,7 @@ exports.bugFixerListFilesTool = {
141
187
  }
142
188
  catch { /* skip */ }
143
189
  };
144
- await scan(args.appPath);
190
+ await scan(appPath);
145
191
  return {
146
192
  content: [{ type: "text", text: JSON.stringify({ files: files.slice(0, 50), total: files.length }) }]
147
193
  };
@@ -233,10 +279,21 @@ Example:
233
279
  }),
234
280
  async execute(args, _context) {
235
281
  try {
236
- // Resolve app path
282
+ // Resolve app path - try absolute first, then search in configured directories
237
283
  let appFullPath = args.appPath;
238
284
  if (!path.isAbsolute(args.appPath)) {
239
- appFullPath = path.join(appsBasePath, args.appPath);
285
+ // Try to find in scanned apps
286
+ const apps = await scanApps();
287
+ const matchedApp = apps.find(a => a.name.toLowerCase() === args.appPath.toLowerCase() ||
288
+ a.path.endsWith(`/${args.appPath}`));
289
+ if (matchedApp) {
290
+ appFullPath = matchedApp.path;
291
+ }
292
+ else {
293
+ // Fallback: try first configured path
294
+ const basePaths = getAppPaths();
295
+ appFullPath = path.join(basePaths[0], args.appPath);
296
+ }
240
297
  }
241
298
  // Check app exists
242
299
  try {
@@ -507,13 +564,366 @@ exports.bugFixerPublishAppTool = {
507
564
  }
508
565
  }
509
566
  };
567
+ // ============================================================================
568
+ // HIGH-LEVEL WORKFLOW TOOLS (LLM-driven workflow control)
569
+ // ============================================================================
570
+ /**
571
+ * Analyze Bug Tool - Read bug report and get classification info
572
+ * The LLM uses this to understand the bug and decide next steps
573
+ */
574
+ exports.bugFixerAnalyzeBugTool = {
575
+ name: "bug_fixer_analyze_bug",
576
+ group: tool_registry_1.ToolGroup.BOT_INTERNAL,
577
+ description: `Read a bug report and get classification information.
578
+ Returns the bug details and classification if already analyzed.
579
+ Use this to understand the current state of a bug discussion.`,
580
+ schema: zod_1.z.object({
581
+ discussionId: zod_1.z.string().describe("The discussion ID for the bug report").optional(),
582
+ activityId: zod_1.z.string().describe("Alias - the bug activity ID (will look up discussion)").optional()
583
+ }).refine(data => data.discussionId || data.activityId, {
584
+ message: "Either discussionId or activityId is required"
585
+ }),
586
+ async execute(args, context) {
587
+ try {
588
+ // Accept either discussionId or look up from activityId
589
+ let discussionId = args.discussionId;
590
+ if (!discussionId && args.activityId) {
591
+ // Try to find discussionId from pending registries by activityId
592
+ const allPending = pending_classification_1.pendingClassificationRegistry.getAll();
593
+ for (const pending of allPending) {
594
+ if (pending.bugId === args.activityId) {
595
+ discussionId = pending.discussionId;
596
+ break;
597
+ }
598
+ }
599
+ // If still not found, fetch activity to get discussion
600
+ if (!discussionId) {
601
+ const activity = await context.hailer.fetchActivityById(args.activityId);
602
+ if (activity?.discussion) {
603
+ discussionId = activity.discussion;
604
+ }
605
+ }
606
+ }
607
+ if (!discussionId) {
608
+ return {
609
+ content: [{
610
+ type: "text",
611
+ text: JSON.stringify({
612
+ status: "error",
613
+ message: "Could not find discussion for the given ID"
614
+ })
615
+ }]
616
+ };
617
+ }
618
+ // Check if we have pending classification info
619
+ const pending = pending_classification_1.pendingClassificationRegistry.get(discussionId);
620
+ if (pending) {
621
+ return {
622
+ content: [{
623
+ type: "text",
624
+ text: JSON.stringify({
625
+ status: "pending_confirmation",
626
+ bugId: pending.bugId,
627
+ bugName: pending.bugName,
628
+ appId: pending.appId,
629
+ appName: pending.appName,
630
+ classification: pending.classification,
631
+ reason: pending.reason,
632
+ description: pending.bug.description,
633
+ stepsToReproduce: pending.bug.stepsToReproduce,
634
+ message: "Bug has been classified. Waiting for user to confirm whether to proceed with fix."
635
+ })
636
+ }]
637
+ };
638
+ }
639
+ // Check if we have a pending fix
640
+ const pendingFix = pending_fix_1.pendingFixRegistry.get(args.discussionId);
641
+ if (pendingFix) {
642
+ return {
643
+ content: [{
644
+ type: "text",
645
+ text: JSON.stringify({
646
+ status: pendingFix.state === "awaiting_test" ? "fix_applied" : "awaiting_explanation",
647
+ bugId: pendingFix.bugId,
648
+ appId: pendingFix.appId,
649
+ fixSummary: pendingFix.fixSummary,
650
+ filesModified: pendingFix.filesModified,
651
+ message: pendingFix.state === "awaiting_test"
652
+ ? "Fix has been applied. Waiting for user to test and approve."
653
+ : "Fix was rejected. Waiting for user to explain what's wrong."
654
+ })
655
+ }]
656
+ };
657
+ }
658
+ return {
659
+ content: [{
660
+ type: "text",
661
+ text: JSON.stringify({
662
+ status: "not_found",
663
+ message: "No pending bug found for this discussion. The bug may have already been processed."
664
+ })
665
+ }]
666
+ };
667
+ }
668
+ catch (e) {
669
+ return {
670
+ content: [{ type: "text", text: JSON.stringify({ error: e.message }) }]
671
+ };
672
+ }
673
+ }
674
+ };
675
+ /**
676
+ * Start Fix Tool - Begin the bug fix process
677
+ * This triggers the full fix workflow: find app → analyze → apply fix → build
678
+ */
679
+ exports.bugFixerStartFixTool = {
680
+ name: "bug_fixer_start_fix",
681
+ group: tool_registry_1.ToolGroup.BOT_INTERNAL,
682
+ description: `Start the bug fix process. Call this when the user confirms they want you to fix the bug.
683
+ This will: find the app, analyze the code, generate and apply a fix, and run the build.
684
+ The user should test the fix afterward.`,
685
+ schema: zod_1.z.object({
686
+ discussionId: zod_1.z.string().describe("The discussion ID for the bug report")
687
+ }),
688
+ async execute(args, _context) {
689
+ try {
690
+ const pending = pending_classification_1.pendingClassificationRegistry.get(args.discussionId);
691
+ if (!pending) {
692
+ return {
693
+ content: [{
694
+ type: "text",
695
+ text: JSON.stringify({
696
+ success: false,
697
+ error: "No pending bug classification found for this discussion. Cannot start fix."
698
+ })
699
+ }]
700
+ };
701
+ }
702
+ // Trigger the fix via the registry callback
703
+ const triggered = await pending_classification_1.pendingClassificationRegistry.triggerFixIt(args.discussionId);
704
+ if (triggered) {
705
+ return {
706
+ content: [{
707
+ type: "text",
708
+ text: JSON.stringify({
709
+ success: true,
710
+ message: "Fix process started. The bot will analyze and apply the fix, then notify the user to test."
711
+ })
712
+ }]
713
+ };
714
+ }
715
+ else {
716
+ return {
717
+ content: [{
718
+ type: "text",
719
+ text: JSON.stringify({
720
+ success: false,
721
+ error: "Failed to trigger fix process. The callback may not be registered."
722
+ })
723
+ }]
724
+ };
725
+ }
726
+ }
727
+ catch (e) {
728
+ return {
729
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }]
730
+ };
731
+ }
732
+ }
733
+ };
734
+ /**
735
+ * Mark Declined Tool - Mark bug as not-a-bug and close it
736
+ */
737
+ exports.bugFixerMarkDeclinedTool = {
738
+ name: "bug_fixer_mark_declined",
739
+ group: tool_registry_1.ToolGroup.BOT_INTERNAL,
740
+ description: `Mark a bug report as "not a bug" and move it to the Declined phase.
741
+ Call this when the user confirms it's actually a feature request or not a real bug.`,
742
+ schema: zod_1.z.object({
743
+ discussionId: zod_1.z.string().describe("The discussion ID for the bug report")
744
+ }),
745
+ async execute(args, _context) {
746
+ try {
747
+ const pending = pending_classification_1.pendingClassificationRegistry.get(args.discussionId);
748
+ if (!pending) {
749
+ return {
750
+ content: [{
751
+ type: "text",
752
+ text: JSON.stringify({
753
+ success: false,
754
+ error: "No pending bug classification found for this discussion."
755
+ })
756
+ }]
757
+ };
758
+ }
759
+ // Trigger the not-a-bug action via the registry callback
760
+ const triggered = await pending_classification_1.pendingClassificationRegistry.triggerNotABug(args.discussionId);
761
+ if (triggered) {
762
+ return {
763
+ content: [{
764
+ type: "text",
765
+ text: JSON.stringify({
766
+ success: true,
767
+ bugId: pending.bugId,
768
+ message: "Bug marked as declined (not a bug). Moved to Declined phase."
769
+ })
770
+ }]
771
+ };
772
+ }
773
+ else {
774
+ return {
775
+ content: [{
776
+ type: "text",
777
+ text: JSON.stringify({
778
+ success: false,
779
+ error: "Failed to decline bug. The callback may not be registered."
780
+ })
781
+ }]
782
+ };
783
+ }
784
+ }
785
+ catch (e) {
786
+ return {
787
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }]
788
+ };
789
+ }
790
+ }
791
+ };
792
+ /**
793
+ * Publish Fix Tool - Publish the fix to production
794
+ */
795
+ exports.bugFixerPublishFixTool = {
796
+ name: "bug_fixer_publish_fix",
797
+ group: tool_registry_1.ToolGroup.BOT_INTERNAL,
798
+ description: `Publish a tested fix to production and mark the bug as Fixed.
799
+ Call this ONLY after the user has tested the fix and approved it.`,
800
+ schema: zod_1.z.object({
801
+ discussionId: zod_1.z.string().describe("The discussion ID for the bug report")
802
+ }),
803
+ async execute(args, _context) {
804
+ try {
805
+ // Trigger approval via the registry callback
806
+ const triggered = await pending_fix_1.pendingFixRegistry.triggerApproval(args.discussionId);
807
+ if (triggered) {
808
+ return {
809
+ content: [{
810
+ type: "text",
811
+ text: JSON.stringify({
812
+ success: true,
813
+ message: "Fix is being published to production. This may take a moment."
814
+ })
815
+ }]
816
+ };
817
+ }
818
+ else {
819
+ // Get more info about why it failed
820
+ const pending = pending_fix_1.pendingFixRegistry.get(args.discussionId);
821
+ if (!pending) {
822
+ return {
823
+ content: [{
824
+ type: "text",
825
+ text: JSON.stringify({
826
+ success: false,
827
+ error: "No pending fix found for this discussion. Cannot publish."
828
+ })
829
+ }]
830
+ };
831
+ }
832
+ if (pending.state !== "awaiting_test") {
833
+ return {
834
+ content: [{
835
+ type: "text",
836
+ text: JSON.stringify({
837
+ success: false,
838
+ error: `Cannot publish - fix is in state '${pending.state}'. User must test and approve first.`
839
+ })
840
+ }]
841
+ };
842
+ }
843
+ return {
844
+ content: [{
845
+ type: "text",
846
+ text: JSON.stringify({
847
+ success: false,
848
+ error: "Failed to trigger publish. The approval callback may not be registered."
849
+ })
850
+ }]
851
+ };
852
+ }
853
+ }
854
+ catch (e) {
855
+ return {
856
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }]
857
+ };
858
+ }
859
+ }
860
+ };
861
+ /**
862
+ * Retry Fix Tool - Retry a fix with additional context/explanation
863
+ */
864
+ exports.bugFixerRetryFixTool = {
865
+ name: "bug_fixer_retry_fix",
866
+ group: tool_registry_1.ToolGroup.BOT_INTERNAL,
867
+ description: `Retry fixing a bug with additional context from the user.
868
+ Call this when the user explains why the previous fix didn't work.`,
869
+ schema: zod_1.z.object({
870
+ discussionId: zod_1.z.string().describe("The discussion ID for the bug report"),
871
+ explanation: zod_1.z.string().describe("User's explanation of what's still broken or wrong")
872
+ }),
873
+ async execute(args, _context) {
874
+ try {
875
+ const pending = pending_fix_1.pendingFixRegistry.get(args.discussionId);
876
+ if (!pending) {
877
+ return {
878
+ content: [{
879
+ type: "text",
880
+ text: JSON.stringify({
881
+ success: false,
882
+ error: "No pending fix found for this discussion."
883
+ })
884
+ }]
885
+ };
886
+ }
887
+ // Trigger retry via the registry callback
888
+ const triggered = await pending_fix_1.pendingFixRegistry.triggerRetry(args.discussionId, args.explanation);
889
+ if (triggered) {
890
+ return {
891
+ content: [{
892
+ type: "text",
893
+ text: JSON.stringify({
894
+ success: true,
895
+ message: "Retry started with user feedback. The bot will generate a new fix."
896
+ })
897
+ }]
898
+ };
899
+ }
900
+ else {
901
+ return {
902
+ content: [{
903
+ type: "text",
904
+ text: JSON.stringify({
905
+ success: false,
906
+ error: "Failed to trigger retry. The callback may not be registered."
907
+ })
908
+ }]
909
+ };
910
+ }
911
+ }
912
+ catch (e) {
913
+ return {
914
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }]
915
+ };
916
+ }
917
+ }
918
+ };
510
919
  // Export all tools
511
920
  exports.bugFixerTools = [
921
+ // Low-level tools
512
922
  exports.bugFixerFindAppTool,
513
923
  exports.bugFixerListFilesTool,
514
924
  exports.bugFixerReadFileTool,
515
925
  exports.bugFixerWriteFileTool,
516
- exports.bugFixerApplyFixTool, // New: search/replace (preferred)
926
+ exports.bugFixerApplyFixTool,
517
927
  exports.bugFixerRunBuildTool,
518
928
  exports.bugFixerGitStatusTool,
519
929
  exports.bugFixerGitPullTool,
@@ -521,5 +931,11 @@ exports.bugFixerTools = [
521
931
  exports.bugFixerGitPushTool,
522
932
  exports.bugFixerGitRevertTool,
523
933
  exports.bugFixerPublishAppTool,
934
+ // High-level workflow tools (LLM-driven)
935
+ exports.bugFixerAnalyzeBugTool,
936
+ exports.bugFixerStartFixTool,
937
+ exports.bugFixerMarkDeclinedTool,
938
+ exports.bugFixerPublishFixTool,
939
+ exports.bugFixerRetryFixTool,
524
940
  ];
525
941
  //# sourceMappingURL=bug-fixer-tools.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Upload Files Tool - Clean Architecture Implementation
2
+ * File Tools - Clean Architecture Implementation
3
3
  *
4
4
  * Demonstrates the new tool architecture pattern:
5
5
  * - Direct Tool<TSchema> interface implementation
@@ -12,4 +12,8 @@ import { Tool } from '../tool-registry';
12
12
  * Upload Files Tool - Clean implementation following new architecture
13
13
  */
14
14
  export declare const uploadFilesTool: Tool;
15
+ /**
16
+ * Download File Tool - Clean implementation
17
+ */
18
+ export declare const downloadFileTool: Tool;
15
19
  //# sourceMappingURL=file.d.ts.map