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.
- package/README.md +30 -0
- package/dist/commands/action.js +47 -20
- package/dist/commands/chat.d.ts +1 -1
- package/dist/commands/chat.js +325 -62
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.d.ts +2 -5
- package/dist/commands/daemon.js +118 -49
- package/dist/commands/docker.js +110 -73
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/help.js +19 -3
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +116 -5
- package/dist/commands/logo.d.ts +5 -0
- package/dist/commands/logo.js +41 -0
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +19 -9
- package/dist/commands/prd.js +20 -2
- package/dist/commands/run.js +111 -27
- package/dist/commands/slack.d.ts +10 -0
- package/dist/commands/slack.js +333 -0
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +6 -1
- package/dist/providers/discord.d.ts +82 -0
- package/dist/providers/discord.js +697 -0
- package/dist/providers/slack.d.ts +79 -0
- package/dist/providers/slack.js +715 -0
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +190 -7
- package/dist/responders/claude-code-responder.d.ts +48 -0
- package/dist/responders/claude-code-responder.js +203 -0
- package/dist/responders/cli-responder.d.ts +62 -0
- package/dist/responders/cli-responder.js +298 -0
- package/dist/responders/llm-responder.d.ts +135 -0
- package/dist/responders/llm-responder.js +582 -0
- package/dist/templates/macos-scripts.js +2 -4
- package/dist/templates/prompts.js +4 -2
- package/dist/tui/ConfigEditor.js +42 -5
- package/dist/tui/components/ArrayEditor.js +1 -1
- package/dist/tui/components/EditorPanel.js +10 -6
- package/dist/tui/components/HelpPanel.d.ts +1 -1
- package/dist/tui/components/HelpPanel.js +1 -1
- package/dist/tui/components/JsonSnippetEditor.js +8 -5
- package/dist/tui/components/KeyValueEditor.js +69 -5
- package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
- package/dist/tui/components/LLMProvidersEditor.js +357 -0
- package/dist/tui/components/ObjectEditor.js +1 -1
- package/dist/tui/components/Preview.js +1 -1
- package/dist/tui/components/RespondersEditor.d.ts +22 -0
- package/dist/tui/components/RespondersEditor.js +437 -0
- package/dist/tui/components/SectionNav.js +27 -3
- package/dist/tui/utils/presets.js +15 -2
- package/dist/utils/chat-client.d.ts +33 -4
- package/dist/utils/chat-client.js +20 -1
- package/dist/utils/config.d.ts +100 -1
- package/dist/utils/config.js +78 -1
- package/dist/utils/daemon-actions.d.ts +19 -0
- package/dist/utils/daemon-actions.js +111 -0
- package/dist/utils/daemon-client.d.ts +21 -0
- package/dist/utils/daemon-client.js +28 -1
- package/dist/utils/llm-client.d.ts +82 -0
- package/dist/utils/llm-client.js +185 -0
- package/dist/utils/message-queue.js +6 -6
- package/dist/utils/notification.d.ts +10 -2
- package/dist/utils/notification.js +111 -4
- package/dist/utils/prd-validator.js +60 -19
- package/dist/utils/prompt.js +22 -12
- package/dist/utils/responder-logger.d.ts +47 -0
- package/dist/utils/responder-logger.js +129 -0
- package/dist/utils/responder-presets.d.ts +92 -0
- package/dist/utils/responder-presets.js +156 -0
- package/dist/utils/responder.d.ts +88 -0
- package/dist/utils/responder.js +207 -0
- package/dist/utils/stream-json.js +6 -6
- package/docs/CHAT-CLIENTS.md +520 -0
- package/docs/CHAT-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/USEFUL_ACTIONS.md +815 -0
- package/docs/chat-architecture.md +251 -0
- 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 +=
|
|
325
|
-
|
|
326
|
-
|
|
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 +=
|
|
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
|
}
|