ralph-cli-sandboxed 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +47 -20
  3. package/dist/commands/chat.d.ts +1 -1
  4. package/dist/commands/chat.js +325 -62
  5. package/dist/commands/config.js +2 -1
  6. package/dist/commands/daemon.d.ts +2 -5
  7. package/dist/commands/daemon.js +118 -49
  8. package/dist/commands/docker.js +110 -73
  9. package/dist/commands/fix-config.js +2 -1
  10. package/dist/commands/fix-prd.js +2 -2
  11. package/dist/commands/help.js +19 -3
  12. package/dist/commands/init.js +78 -17
  13. package/dist/commands/listen.js +116 -5
  14. package/dist/commands/logo.d.ts +5 -0
  15. package/dist/commands/logo.js +41 -0
  16. package/dist/commands/notify.js +1 -1
  17. package/dist/commands/once.js +19 -9
  18. package/dist/commands/prd.js +20 -2
  19. package/dist/commands/run.js +111 -27
  20. package/dist/commands/slack.d.ts +10 -0
  21. package/dist/commands/slack.js +333 -0
  22. package/dist/config/responder-presets.json +69 -0
  23. package/dist/index.js +6 -1
  24. package/dist/providers/discord.d.ts +82 -0
  25. package/dist/providers/discord.js +697 -0
  26. package/dist/providers/slack.d.ts +79 -0
  27. package/dist/providers/slack.js +715 -0
  28. package/dist/providers/telegram.d.ts +30 -0
  29. package/dist/providers/telegram.js +190 -7
  30. package/dist/responders/claude-code-responder.d.ts +48 -0
  31. package/dist/responders/claude-code-responder.js +203 -0
  32. package/dist/responders/cli-responder.d.ts +62 -0
  33. package/dist/responders/cli-responder.js +298 -0
  34. package/dist/responders/llm-responder.d.ts +135 -0
  35. package/dist/responders/llm-responder.js +582 -0
  36. package/dist/templates/macos-scripts.js +2 -4
  37. package/dist/templates/prompts.js +4 -2
  38. package/dist/tui/ConfigEditor.js +42 -5
  39. package/dist/tui/components/ArrayEditor.js +1 -1
  40. package/dist/tui/components/EditorPanel.js +10 -6
  41. package/dist/tui/components/HelpPanel.d.ts +1 -1
  42. package/dist/tui/components/HelpPanel.js +1 -1
  43. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  44. package/dist/tui/components/KeyValueEditor.js +69 -5
  45. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  46. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  47. package/dist/tui/components/ObjectEditor.js +1 -1
  48. package/dist/tui/components/Preview.js +1 -1
  49. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  50. package/dist/tui/components/RespondersEditor.js +437 -0
  51. package/dist/tui/components/SectionNav.js +27 -3
  52. package/dist/tui/utils/presets.js +15 -2
  53. package/dist/utils/chat-client.d.ts +33 -4
  54. package/dist/utils/chat-client.js +20 -1
  55. package/dist/utils/config.d.ts +100 -1
  56. package/dist/utils/config.js +78 -1
  57. package/dist/utils/daemon-actions.d.ts +19 -0
  58. package/dist/utils/daemon-actions.js +111 -0
  59. package/dist/utils/daemon-client.d.ts +21 -0
  60. package/dist/utils/daemon-client.js +28 -1
  61. package/dist/utils/llm-client.d.ts +82 -0
  62. package/dist/utils/llm-client.js +185 -0
  63. package/dist/utils/message-queue.js +6 -6
  64. package/dist/utils/notification.d.ts +10 -2
  65. package/dist/utils/notification.js +111 -4
  66. package/dist/utils/prd-validator.js +60 -19
  67. package/dist/utils/prompt.js +22 -12
  68. package/dist/utils/responder-logger.d.ts +47 -0
  69. package/dist/utils/responder-logger.js +129 -0
  70. package/dist/utils/responder-presets.d.ts +92 -0
  71. package/dist/utils/responder-presets.js +156 -0
  72. package/dist/utils/responder.d.ts +88 -0
  73. package/dist/utils/responder.js +207 -0
  74. package/dist/utils/stream-json.js +6 -6
  75. package/docs/CHAT-CLIENTS.md +520 -0
  76. package/docs/CHAT-RESPONDERS.md +785 -0
  77. package/docs/DEVELOPMENT.md +25 -0
  78. package/docs/USEFUL_ACTIONS.md +815 -0
  79. package/docs/chat-architecture.md +251 -0
  80. package/package.json +14 -1
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Logger for responder calls.
3
+ * Logs LLM requests to .ralph/logs/ for debugging and auditing.
4
+ */
5
+ import { mkdirSync, appendFileSync, existsSync } from "fs";
6
+ import { join } from "path";
7
+ /**
8
+ * Get the logs directory path (.ralph/logs).
9
+ */
10
+ function getLogsDir() {
11
+ return join(process.cwd(), ".ralph", "logs");
12
+ }
13
+ /**
14
+ * Ensure the logs directory exists.
15
+ */
16
+ function ensureLogsDir() {
17
+ const logsDir = getLogsDir();
18
+ if (!existsSync(logsDir)) {
19
+ mkdirSync(logsDir, { recursive: true });
20
+ }
21
+ }
22
+ /**
23
+ * Get today's log file path.
24
+ */
25
+ function getLogFilePath() {
26
+ const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
27
+ return join(getLogsDir(), `responder-${date}.log`);
28
+ }
29
+ /**
30
+ * Format a log entry for file output.
31
+ */
32
+ function formatLogEntry(entry) {
33
+ const lines = [
34
+ ``,
35
+ `================================================================================`,
36
+ `[${entry.timestamp}] ${entry.responderName || "unknown"} (${entry.responderType || "unknown"})`,
37
+ `================================================================================`,
38
+ ];
39
+ if (entry.trigger) {
40
+ lines.push(`Trigger: ${entry.trigger}`);
41
+ }
42
+ if (entry.gitCommand) {
43
+ lines.push(`Git command: ${entry.gitCommand}`);
44
+ lines.push(`Git diff length: ${entry.gitDiffLength || 0} chars`);
45
+ }
46
+ if (entry.filesRead && entry.filesRead.length > 0) {
47
+ lines.push(`Files read: ${entry.filesRead.join(", ")}`);
48
+ lines.push(`Files total length: ${entry.filesTotalLength || 0} chars`);
49
+ }
50
+ if (entry.filesNotFound && entry.filesNotFound.length > 0) {
51
+ lines.push(`Files not found: ${entry.filesNotFound.join(", ")}`);
52
+ }
53
+ if (entry.threadContextLength) {
54
+ lines.push(`Thread context: ${entry.threadContextLength} chars`);
55
+ }
56
+ lines.push(`Message length: ${entry.messageLength} chars`);
57
+ lines.push(``);
58
+ if (entry.systemPrompt) {
59
+ lines.push(`--- System Prompt ---`);
60
+ lines.push(entry.systemPrompt);
61
+ lines.push(``);
62
+ }
63
+ lines.push(`--- Message to LLM ---`);
64
+ lines.push(entry.message);
65
+ lines.push(``);
66
+ return lines.join("\n");
67
+ }
68
+ /**
69
+ * Log a responder call to the log file.
70
+ */
71
+ export function logResponderCall(entry) {
72
+ try {
73
+ ensureLogsDir();
74
+ const logFile = getLogFilePath();
75
+ const formatted = formatLogEntry(entry);
76
+ appendFileSync(logFile, formatted, "utf-8");
77
+ }
78
+ catch {
79
+ // Silently ignore logging errors to not disrupt the main flow
80
+ }
81
+ }
82
+ /**
83
+ * Log a responder call to console (for debug mode).
84
+ */
85
+ export function logResponderCallToConsole(entry) {
86
+ console.log(`[responder] ${entry.responderName} (${entry.responderType})`);
87
+ if (entry.gitCommand) {
88
+ console.log(`[responder] Git command: ${entry.gitCommand}`);
89
+ console.log(`[responder] Git diff: ${entry.gitDiffLength || 0} chars`);
90
+ }
91
+ if (entry.filesRead && entry.filesRead.length > 0) {
92
+ console.log(`[responder] Files read: ${entry.filesRead.join(", ")}`);
93
+ console.log(`[responder] Files total: ${entry.filesTotalLength || 0} chars`);
94
+ }
95
+ if (entry.filesNotFound && entry.filesNotFound.length > 0) {
96
+ console.log(`[responder] Files not found: ${entry.filesNotFound.join(", ")}`);
97
+ }
98
+ if (entry.threadContextLength) {
99
+ console.log(`[responder] Thread context: ${entry.threadContextLength} chars`);
100
+ }
101
+ console.log(`[responder] Total message: ${entry.messageLength} chars`);
102
+ console.log(`[responder] Log file: ${getLogFilePath()}`);
103
+ }
104
+ /**
105
+ * Create a log entry and optionally log to console.
106
+ */
107
+ export function createResponderLog(options) {
108
+ const entry = {
109
+ timestamp: new Date().toISOString(),
110
+ responderName: options.responderName,
111
+ responderType: options.responderType,
112
+ trigger: options.trigger,
113
+ gitCommand: options.gitCommand,
114
+ gitDiffLength: options.gitDiffLength,
115
+ filesRead: options.filesRead,
116
+ filesNotFound: options.filesNotFound,
117
+ filesTotalLength: options.filesTotalLength,
118
+ threadContextLength: options.threadContextLength,
119
+ messageLength: options.message.length,
120
+ message: options.message,
121
+ systemPrompt: options.systemPrompt,
122
+ };
123
+ // Always log to file
124
+ logResponderCall(entry);
125
+ // Log to console if debug mode
126
+ if (options.debug) {
127
+ logResponderCallToConsole(entry);
128
+ }
129
+ }
@@ -0,0 +1,92 @@
1
+ import { type ResponderConfig, type RespondersConfig } from "./config.js";
2
+ /**
3
+ * Preset responder configuration from the JSON file.
4
+ */
5
+ export interface ResponderPreset {
6
+ name: string;
7
+ description: string;
8
+ type: "llm" | "claude-code" | "cli";
9
+ trigger?: string;
10
+ provider?: string;
11
+ systemPrompt?: string;
12
+ command?: string;
13
+ timeout?: number;
14
+ maxLength?: number;
15
+ }
16
+ /**
17
+ * Bundle of presets for quick setup.
18
+ */
19
+ export interface ResponderBundle {
20
+ name: string;
21
+ description: string;
22
+ presets: string[];
23
+ }
24
+ /**
25
+ * Full presets configuration from the JSON file.
26
+ */
27
+ export interface ResponderPresetsConfig {
28
+ presets: Record<string, ResponderPreset>;
29
+ bundles: Record<string, ResponderBundle>;
30
+ }
31
+ /**
32
+ * Load responder presets from the configuration file.
33
+ */
34
+ export declare function loadResponderPresets(): ResponderPresetsConfig;
35
+ /**
36
+ * Get a single preset by ID.
37
+ */
38
+ export declare function getResponderPreset(presetId: string): ResponderPreset | undefined;
39
+ /**
40
+ * Get all available preset IDs.
41
+ */
42
+ export declare function getResponderPresetIds(): string[];
43
+ /**
44
+ * Get all available presets with their metadata.
45
+ */
46
+ export declare function getResponderPresetList(): Array<{
47
+ id: string;
48
+ } & ResponderPreset>;
49
+ /**
50
+ * Get all available bundles.
51
+ */
52
+ export declare function getResponderBundles(): Record<string, ResponderBundle>;
53
+ /**
54
+ * Get a single bundle by ID.
55
+ */
56
+ export declare function getResponderBundle(bundleId: string): ResponderBundle | undefined;
57
+ /**
58
+ * Get all available bundle IDs.
59
+ */
60
+ export declare function getResponderBundleIds(): string[];
61
+ /**
62
+ * Convert a preset to a ResponderConfig for use in config.json.
63
+ * Strips the name and description fields that are only for display.
64
+ */
65
+ export declare function presetToResponderConfig(preset: ResponderPreset): ResponderConfig;
66
+ /**
67
+ * Convert multiple preset IDs to a RespondersConfig object.
68
+ * Uses the preset ID as the responder name.
69
+ */
70
+ export declare function presetsToRespondersConfig(presetIds: string[]): RespondersConfig;
71
+ /**
72
+ * Convert a bundle to a RespondersConfig object.
73
+ */
74
+ export declare function bundleToRespondersConfig(bundleId: string): RespondersConfig;
75
+ /**
76
+ * Get display options for preset selection in CLI prompts.
77
+ * Returns array of "name - description" strings for display.
78
+ */
79
+ export declare function getPresetDisplayOptions(): string[];
80
+ /**
81
+ * Get display options for bundle selection in CLI prompts.
82
+ * Returns array of "name - description (preset1, preset2, ...)" strings for display.
83
+ */
84
+ export declare function getBundleDisplayOptions(): string[];
85
+ /**
86
+ * Map bundle display option back to bundle ID.
87
+ */
88
+ export declare function displayOptionToBundleId(displayOption: string): string | undefined;
89
+ /**
90
+ * Map preset display option back to preset ID.
91
+ */
92
+ export declare function displayOptionToPresetId(displayOption: string): string | undefined;
@@ -0,0 +1,156 @@
1
+ import { readFileSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = dirname(__filename);
6
+ // Path to the presets file (relative to dist/utils when compiled)
7
+ const PRESETS_FILE = join(__dirname, "..", "config", "responder-presets.json");
8
+ /**
9
+ * Load responder presets from the configuration file.
10
+ */
11
+ export function loadResponderPresets() {
12
+ try {
13
+ const content = readFileSync(PRESETS_FILE, "utf-8");
14
+ return JSON.parse(content);
15
+ }
16
+ catch (error) {
17
+ console.error(`Failed to load responder presets: ${error}`);
18
+ return { presets: {}, bundles: {} };
19
+ }
20
+ }
21
+ /**
22
+ * Get a single preset by ID.
23
+ */
24
+ export function getResponderPreset(presetId) {
25
+ const config = loadResponderPresets();
26
+ return config.presets[presetId];
27
+ }
28
+ /**
29
+ * Get all available preset IDs.
30
+ */
31
+ export function getResponderPresetIds() {
32
+ const config = loadResponderPresets();
33
+ return Object.keys(config.presets);
34
+ }
35
+ /**
36
+ * Get all available presets with their metadata.
37
+ */
38
+ export function getResponderPresetList() {
39
+ const config = loadResponderPresets();
40
+ return Object.entries(config.presets).map(([id, preset]) => ({
41
+ id,
42
+ ...preset,
43
+ }));
44
+ }
45
+ /**
46
+ * Get all available bundles.
47
+ */
48
+ export function getResponderBundles() {
49
+ const config = loadResponderPresets();
50
+ return config.bundles;
51
+ }
52
+ /**
53
+ * Get a single bundle by ID.
54
+ */
55
+ export function getResponderBundle(bundleId) {
56
+ const config = loadResponderPresets();
57
+ return config.bundles[bundleId];
58
+ }
59
+ /**
60
+ * Get all available bundle IDs.
61
+ */
62
+ export function getResponderBundleIds() {
63
+ const config = loadResponderPresets();
64
+ return Object.keys(config.bundles);
65
+ }
66
+ /**
67
+ * Convert a preset to a ResponderConfig for use in config.json.
68
+ * Strips the name and description fields that are only for display.
69
+ */
70
+ export function presetToResponderConfig(preset) {
71
+ const config = {
72
+ type: preset.type,
73
+ };
74
+ if (preset.trigger)
75
+ config.trigger = preset.trigger;
76
+ if (preset.provider)
77
+ config.provider = preset.provider;
78
+ if (preset.systemPrompt)
79
+ config.systemPrompt = preset.systemPrompt;
80
+ if (preset.command)
81
+ config.command = preset.command;
82
+ if (preset.timeout)
83
+ config.timeout = preset.timeout;
84
+ if (preset.maxLength)
85
+ config.maxLength = preset.maxLength;
86
+ return config;
87
+ }
88
+ /**
89
+ * Convert multiple preset IDs to a RespondersConfig object.
90
+ * Uses the preset ID as the responder name.
91
+ */
92
+ export function presetsToRespondersConfig(presetIds) {
93
+ const config = loadResponderPresets();
94
+ const responders = {};
95
+ for (const id of presetIds) {
96
+ const preset = config.presets[id];
97
+ if (preset) {
98
+ responders[id] = presetToResponderConfig(preset);
99
+ }
100
+ }
101
+ return responders;
102
+ }
103
+ /**
104
+ * Convert a bundle to a RespondersConfig object.
105
+ */
106
+ export function bundleToRespondersConfig(bundleId) {
107
+ const config = loadResponderPresets();
108
+ const bundle = config.bundles[bundleId];
109
+ if (!bundle) {
110
+ return {};
111
+ }
112
+ return presetsToRespondersConfig(bundle.presets);
113
+ }
114
+ /**
115
+ * Get display options for preset selection in CLI prompts.
116
+ * Returns array of "name - description" strings for display.
117
+ */
118
+ export function getPresetDisplayOptions() {
119
+ const presets = getResponderPresetList();
120
+ return presets.map((p) => `${p.name} - ${p.description}`);
121
+ }
122
+ /**
123
+ * Get display options for bundle selection in CLI prompts.
124
+ * Returns array of "name - description (preset1, preset2, ...)" strings for display.
125
+ */
126
+ export function getBundleDisplayOptions() {
127
+ const config = loadResponderPresets();
128
+ return Object.entries(config.bundles).map(([, bundle]) => {
129
+ const presetNames = bundle.presets.join(", ");
130
+ return `${bundle.name} - ${bundle.description} (${presetNames})`;
131
+ });
132
+ }
133
+ /**
134
+ * Map bundle display option back to bundle ID.
135
+ */
136
+ export function displayOptionToBundleId(displayOption) {
137
+ const config = loadResponderPresets();
138
+ for (const [id, bundle] of Object.entries(config.bundles)) {
139
+ if (displayOption.startsWith(bundle.name)) {
140
+ return id;
141
+ }
142
+ }
143
+ return undefined;
144
+ }
145
+ /**
146
+ * Map preset display option back to preset ID.
147
+ */
148
+ export function displayOptionToPresetId(displayOption) {
149
+ const config = loadResponderPresets();
150
+ for (const [id, preset] of Object.entries(config.presets)) {
151
+ if (displayOption.startsWith(preset.name)) {
152
+ return id;
153
+ }
154
+ }
155
+ return undefined;
156
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Responder matching and routing for chat messages.
3
+ * Matches incoming messages to configured responders based on trigger patterns.
4
+ */
5
+ import { ResponderConfig, RespondersConfig } from "./config.js";
6
+ /**
7
+ * Result of matching a message to a responder.
8
+ */
9
+ export interface ResponderMatch {
10
+ /** The matched responder name (key in responders config) */
11
+ name: string;
12
+ /** The matched responder configuration */
13
+ responder: ResponderConfig;
14
+ /** The remaining message after removing the trigger (the arguments to pass to the responder) */
15
+ args: string;
16
+ }
17
+ /**
18
+ * ResponderMatcher handles routing chat messages to the appropriate responder
19
+ * based on configured trigger patterns.
20
+ *
21
+ * Trigger patterns:
22
+ * - "@name" triggers: Match when message starts with @name (e.g., "@qa", "@code", "@review")
23
+ * - "keyword" triggers: Match when message starts with the keyword (e.g., "!lint", "help")
24
+ * - No trigger (default): Matches any message that doesn't match other triggers
25
+ *
26
+ * The special "default" responder handles messages that don't match any trigger.
27
+ */
28
+ export declare class ResponderMatcher {
29
+ private responders;
30
+ private mentionTriggers;
31
+ private keywordTriggers;
32
+ private defaultResponderName;
33
+ /**
34
+ * Create a new ResponderMatcher with the given responder configurations.
35
+ * @param responders The responders configuration (name -> config map)
36
+ */
37
+ constructor(responders: RespondersConfig);
38
+ /**
39
+ * Match a message to a responder based on trigger patterns.
40
+ *
41
+ * @param message The incoming message text
42
+ * @returns The matched responder and remaining args, or null if no match and no default
43
+ */
44
+ matchResponder(message: string): ResponderMatch | null;
45
+ /**
46
+ * Match @mention triggers in a message.
47
+ * First tries to match at the start of the message, then looks for triggers anywhere.
48
+ * Handles variations like "@qa", "@qa ", "@qa: ", etc.
49
+ */
50
+ private matchMentionTrigger;
51
+ /**
52
+ * Match keyword triggers at the start of a message.
53
+ */
54
+ private matchKeywordTrigger;
55
+ /**
56
+ * Extract the remaining message after a trigger, handling common separators.
57
+ */
58
+ private extractArgsAfterTrigger;
59
+ /**
60
+ * Get the default responder match, or null if no default is configured.
61
+ */
62
+ private getDefaultMatch;
63
+ /**
64
+ * Get all configured responder names.
65
+ */
66
+ getResponderNames(): string[];
67
+ /**
68
+ * Get a specific responder by name.
69
+ */
70
+ getResponder(name: string): ResponderConfig | undefined;
71
+ /**
72
+ * Check if a default responder is configured.
73
+ */
74
+ hasDefaultResponder(): boolean;
75
+ /**
76
+ * Get the default responder name if configured.
77
+ */
78
+ getDefaultResponderName(): string | null;
79
+ }
80
+ /**
81
+ * Convenience function to match a message against a responders config.
82
+ * Creates a ResponderMatcher and performs the match in one call.
83
+ *
84
+ * @param message The message to match
85
+ * @param responders The responders configuration
86
+ * @returns The match result or null
87
+ */
88
+ export declare function matchResponder(message: string, responders: RespondersConfig): ResponderMatch | null;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Responder matching and routing for chat messages.
3
+ * Matches incoming messages to configured responders based on trigger patterns.
4
+ */
5
+ /**
6
+ * ResponderMatcher handles routing chat messages to the appropriate responder
7
+ * based on configured trigger patterns.
8
+ *
9
+ * Trigger patterns:
10
+ * - "@name" triggers: Match when message starts with @name (e.g., "@qa", "@code", "@review")
11
+ * - "keyword" triggers: Match when message starts with the keyword (e.g., "!lint", "help")
12
+ * - No trigger (default): Matches any message that doesn't match other triggers
13
+ *
14
+ * The special "default" responder handles messages that don't match any trigger.
15
+ */
16
+ export class ResponderMatcher {
17
+ responders;
18
+ mentionTriggers; // trigger -> responder name
19
+ keywordTriggers; // trigger -> responder name
20
+ defaultResponderName;
21
+ /**
22
+ * Create a new ResponderMatcher with the given responder configurations.
23
+ * @param responders The responders configuration (name -> config map)
24
+ */
25
+ constructor(responders) {
26
+ this.responders = new Map();
27
+ this.mentionTriggers = new Map();
28
+ this.keywordTriggers = new Map();
29
+ this.defaultResponderName = null;
30
+ // Process responders and categorize triggers
31
+ for (const [name, config] of Object.entries(responders)) {
32
+ this.responders.set(name, config);
33
+ // Check for default responder (no trigger or name is "default")
34
+ if (!config.trigger || name === "default") {
35
+ this.defaultResponderName = name;
36
+ continue;
37
+ }
38
+ const trigger = config.trigger.trim();
39
+ // @mention triggers (e.g., "@qa", "@code")
40
+ if (trigger.startsWith("@")) {
41
+ this.mentionTriggers.set(trigger.toLowerCase(), name);
42
+ }
43
+ else {
44
+ // Keyword triggers (e.g., "!lint", "help")
45
+ this.keywordTriggers.set(trigger.toLowerCase(), name);
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Match a message to a responder based on trigger patterns.
51
+ *
52
+ * @param message The incoming message text
53
+ * @returns The matched responder and remaining args, or null if no match and no default
54
+ */
55
+ matchResponder(message) {
56
+ const trimmed = message.trim();
57
+ if (!trimmed) {
58
+ return this.getDefaultMatch("");
59
+ }
60
+ // Try to match @mention triggers first (highest priority)
61
+ const mentionMatch = this.matchMentionTrigger(trimmed);
62
+ if (mentionMatch) {
63
+ return mentionMatch;
64
+ }
65
+ // Try to match keyword triggers
66
+ const keywordMatch = this.matchKeywordTrigger(trimmed);
67
+ if (keywordMatch) {
68
+ return keywordMatch;
69
+ }
70
+ // Fall back to default responder
71
+ return this.getDefaultMatch(trimmed);
72
+ }
73
+ /**
74
+ * Match @mention triggers in a message.
75
+ * First tries to match at the start of the message, then looks for triggers anywhere.
76
+ * Handles variations like "@qa", "@qa ", "@qa: ", etc.
77
+ */
78
+ matchMentionTrigger(message) {
79
+ // First, try to match at the start of the message (highest priority)
80
+ if (message.startsWith("@")) {
81
+ const startMatch = message.match(/^(@\w+)/i);
82
+ if (startMatch) {
83
+ const mention = startMatch[1].toLowerCase();
84
+ const responderName = this.mentionTriggers.get(mention);
85
+ if (responderName) {
86
+ const responder = this.responders.get(responderName);
87
+ if (responder) {
88
+ const args = this.extractArgsAfterTrigger(message, mention.length);
89
+ return { name: responderName, responder, args };
90
+ }
91
+ }
92
+ }
93
+ }
94
+ // If not at start, look for @trigger anywhere in the message
95
+ // This allows patterns like "need help @qa explain this"
96
+ for (const [trigger] of this.mentionTriggers) {
97
+ // Build regex to find trigger as a word boundary (not part of email, etc.)
98
+ // Match trigger preceded by whitespace or start, followed by whitespace, colon, or end
99
+ const escapedTrigger = trigger.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
100
+ const regex = new RegExp(`(?:^|\\s)(${escapedTrigger})(?:[:\\s]|$)`, "i");
101
+ const match = message.match(regex);
102
+ if (match) {
103
+ const responderName = this.mentionTriggers.get(trigger);
104
+ if (!responderName)
105
+ continue;
106
+ const responder = this.responders.get(responderName);
107
+ if (!responder)
108
+ continue;
109
+ // Extract args: everything after the trigger
110
+ const triggerIndex = match.index + match[0].indexOf(match[1]);
111
+ const afterTrigger = message.slice(triggerIndex + match[1].length);
112
+ const args = afterTrigger.replace(/^[:]\s*/, "").replace(/^\s+/, "").trim();
113
+ return { name: responderName, responder, args };
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ /**
119
+ * Match keyword triggers at the start of a message.
120
+ */
121
+ matchKeywordTrigger(message) {
122
+ const lowerMessage = message.toLowerCase();
123
+ // Check each keyword trigger
124
+ for (const [trigger, responderName] of this.keywordTriggers) {
125
+ // Match if message starts with the trigger followed by whitespace or end of message
126
+ if (lowerMessage === trigger ||
127
+ lowerMessage.startsWith(trigger + " ") ||
128
+ lowerMessage.startsWith(trigger + ":") ||
129
+ lowerMessage.startsWith(trigger + "\n")) {
130
+ const responder = this.responders.get(responderName);
131
+ if (!responder) {
132
+ continue;
133
+ }
134
+ const args = this.extractArgsAfterTrigger(message, trigger.length);
135
+ return {
136
+ name: responderName,
137
+ responder,
138
+ args,
139
+ };
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ /**
145
+ * Extract the remaining message after a trigger, handling common separators.
146
+ */
147
+ extractArgsAfterTrigger(message, triggerLength) {
148
+ let remaining = message.slice(triggerLength);
149
+ // Remove leading separator if present (: or whitespace)
150
+ remaining = remaining.replace(/^[:]\s*/, "");
151
+ remaining = remaining.replace(/^\s+/, "");
152
+ return remaining.trim();
153
+ }
154
+ /**
155
+ * Get the default responder match, or null if no default is configured.
156
+ */
157
+ getDefaultMatch(args) {
158
+ if (!this.defaultResponderName) {
159
+ return null;
160
+ }
161
+ const responder = this.responders.get(this.defaultResponderName);
162
+ if (!responder) {
163
+ return null;
164
+ }
165
+ return {
166
+ name: this.defaultResponderName,
167
+ responder,
168
+ args,
169
+ };
170
+ }
171
+ /**
172
+ * Get all configured responder names.
173
+ */
174
+ getResponderNames() {
175
+ return Array.from(this.responders.keys());
176
+ }
177
+ /**
178
+ * Get a specific responder by name.
179
+ */
180
+ getResponder(name) {
181
+ return this.responders.get(name);
182
+ }
183
+ /**
184
+ * Check if a default responder is configured.
185
+ */
186
+ hasDefaultResponder() {
187
+ return this.defaultResponderName !== null;
188
+ }
189
+ /**
190
+ * Get the default responder name if configured.
191
+ */
192
+ getDefaultResponderName() {
193
+ return this.defaultResponderName;
194
+ }
195
+ }
196
+ /**
197
+ * Convenience function to match a message against a responders config.
198
+ * Creates a ResponderMatcher and performs the match in one call.
199
+ *
200
+ * @param message The message to match
201
+ * @param responders The responders configuration
202
+ * @returns The match result or null
203
+ */
204
+ export function matchResponder(message, responders) {
205
+ const matcher = new ResponderMatcher(responders);
206
+ return matcher.matchResponder(message);
207
+ }
@@ -321,9 +321,10 @@ export class OpenCodeStreamParser extends BaseStreamParser {
321
321
  let toolOutput = `\n── Tool: ${json.name || json.tool} ──\n`;
322
322
  if (json.input || json.args || json.arguments) {
323
323
  const toolInput = json.input || json.args || json.arguments;
324
- toolOutput += typeof toolInput === "string"
325
- ? toolInput + "\n"
326
- : JSON.stringify(toolInput, null, 2) + "\n";
324
+ toolOutput +=
325
+ typeof toolInput === "string"
326
+ ? toolInput + "\n"
327
+ : JSON.stringify(toolInput, null, 2) + "\n";
327
328
  }
328
329
  return toolOutput;
329
330
  }
@@ -552,9 +553,8 @@ export class AiderStreamParser extends BaseStreamParser {
552
553
  let toolOutput = `\n── Tool: ${json.name || json.function} ──\n`;
553
554
  if (json.arguments || json.args) {
554
555
  const args = json.arguments || json.args;
555
- toolOutput += typeof args === "string"
556
- ? args + "\n"
557
- : JSON.stringify(args, null, 2) + "\n";
556
+ toolOutput +=
557
+ typeof args === "string" ? args + "\n" : JSON.stringify(args, null, 2) + "\n";
558
558
  }
559
559
  return toolOutput;
560
560
  }