ralph-cli-sandboxed 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -15
- package/dist/commands/chat.d.ts +8 -0
- package/dist/commands/chat.js +613 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +63 -0
- package/dist/commands/daemon.d.ts +23 -0
- package/dist/commands/daemon.js +422 -0
- package/dist/commands/docker.js +82 -4
- package/dist/commands/help.js +63 -0
- package/dist/commands/init.js +47 -0
- package/dist/commands/listen.d.ts +8 -0
- package/dist/commands/listen.js +239 -0
- package/dist/commands/notify.d.ts +7 -0
- package/dist/commands/notify.js +165 -0
- package/dist/commands/once.js +8 -8
- package/dist/commands/run.js +25 -12
- package/dist/index.js +10 -0
- package/dist/providers/telegram.d.ts +35 -0
- package/dist/providers/telegram.js +190 -0
- package/dist/utils/chat-client.d.ts +114 -0
- package/dist/utils/chat-client.js +76 -0
- package/dist/utils/config.d.ts +47 -0
- package/dist/utils/daemon-client.d.ts +36 -0
- package/dist/utils/daemon-client.js +70 -0
- package/dist/utils/message-queue.d.ts +58 -0
- package/dist/utils/message-queue.js +133 -0
- package/dist/utils/notification.d.ts +28 -1
- package/dist/utils/notification.js +146 -20
- package/docs/RALPH-SETUP-TEMPLATE.md +262 -0
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,11 @@ import { prd, prdAdd, prdList, prdStatus, prdToggle, prdClean, parseListArgs } f
|
|
|
10
10
|
import { docker } from "./commands/docker.js";
|
|
11
11
|
import { prompt } from "./commands/prompt.js";
|
|
12
12
|
import { fixPrd } from "./commands/fix-prd.js";
|
|
13
|
+
import { daemon } from "./commands/daemon.js";
|
|
14
|
+
import { notify } from "./commands/notify.js";
|
|
15
|
+
import { chat } from "./commands/chat.js";
|
|
16
|
+
import { listen } from "./commands/listen.js";
|
|
17
|
+
import { config } from "./commands/config.js";
|
|
13
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
19
|
const __dirname = dirname(__filename);
|
|
15
20
|
function getPackageInfo() {
|
|
@@ -25,6 +30,11 @@ const commands = {
|
|
|
25
30
|
prd,
|
|
26
31
|
prompt,
|
|
27
32
|
docker,
|
|
33
|
+
daemon,
|
|
34
|
+
notify,
|
|
35
|
+
chat,
|
|
36
|
+
listen,
|
|
37
|
+
config,
|
|
28
38
|
"fix-prd": (args) => fixPrd(args),
|
|
29
39
|
// Top-level PRD commands (shortcuts)
|
|
30
40
|
add: () => prdAdd(),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram chat client implementation.
|
|
3
|
+
* Uses the Telegram Bot API with long polling to receive messages.
|
|
4
|
+
*/
|
|
5
|
+
import { ChatClient, ChatCommandHandler, ChatMessageHandler, TelegramSettings } from "../utils/chat-client.js";
|
|
6
|
+
export declare class TelegramChatClient implements ChatClient {
|
|
7
|
+
readonly provider: "telegram";
|
|
8
|
+
private settings;
|
|
9
|
+
private connected;
|
|
10
|
+
private polling;
|
|
11
|
+
private lastUpdateId;
|
|
12
|
+
private pollingTimeout;
|
|
13
|
+
private debug;
|
|
14
|
+
constructor(settings: TelegramSettings, debug?: boolean);
|
|
15
|
+
/**
|
|
16
|
+
* Make a request to the Telegram Bot API.
|
|
17
|
+
*/
|
|
18
|
+
private apiRequest;
|
|
19
|
+
/**
|
|
20
|
+
* Check if a chat ID is allowed.
|
|
21
|
+
*/
|
|
22
|
+
private isChatAllowed;
|
|
23
|
+
/**
|
|
24
|
+
* Start long polling for updates.
|
|
25
|
+
*/
|
|
26
|
+
private poll;
|
|
27
|
+
connect(onCommand: ChatCommandHandler, onMessage?: ChatMessageHandler): Promise<void>;
|
|
28
|
+
sendMessage(chatId: string, text: string): Promise<void>;
|
|
29
|
+
disconnect(): Promise<void>;
|
|
30
|
+
isConnected(): boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a Telegram chat client from settings.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createTelegramClient(settings: TelegramSettings, debug?: boolean): ChatClient;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram chat client implementation.
|
|
3
|
+
* Uses the Telegram Bot API with long polling to receive messages.
|
|
4
|
+
*/
|
|
5
|
+
import https from "https";
|
|
6
|
+
import { parseCommand, } from "../utils/chat-client.js";
|
|
7
|
+
export class TelegramChatClient {
|
|
8
|
+
provider = "telegram";
|
|
9
|
+
settings;
|
|
10
|
+
connected = false;
|
|
11
|
+
polling = false;
|
|
12
|
+
lastUpdateId = 0;
|
|
13
|
+
pollingTimeout = null;
|
|
14
|
+
debug;
|
|
15
|
+
constructor(settings, debug = false) {
|
|
16
|
+
this.settings = settings;
|
|
17
|
+
this.debug = debug;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Make a request to the Telegram Bot API.
|
|
21
|
+
*/
|
|
22
|
+
async apiRequest(method, body) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const url = `https://api.telegram.org/bot${this.settings.botToken}/${method}`;
|
|
25
|
+
const options = {
|
|
26
|
+
method: body ? "POST" : "GET",
|
|
27
|
+
headers: body
|
|
28
|
+
? {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
}
|
|
31
|
+
: undefined,
|
|
32
|
+
};
|
|
33
|
+
const req = https.request(url, options, (res) => {
|
|
34
|
+
let data = "";
|
|
35
|
+
res.on("data", (chunk) => {
|
|
36
|
+
data += chunk;
|
|
37
|
+
});
|
|
38
|
+
res.on("end", () => {
|
|
39
|
+
try {
|
|
40
|
+
const response = JSON.parse(data);
|
|
41
|
+
if (response.ok && response.result !== undefined) {
|
|
42
|
+
resolve(response.result);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
reject(new Error(`Telegram API error: ${response.description || "Unknown error"} (code: ${response.error_code})`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
reject(new Error(`Failed to parse Telegram response: ${data}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
req.on("error", (err) => {
|
|
54
|
+
reject(new Error(`Telegram request failed: ${err.message}`));
|
|
55
|
+
});
|
|
56
|
+
if (body) {
|
|
57
|
+
req.write(JSON.stringify(body));
|
|
58
|
+
}
|
|
59
|
+
req.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if a chat ID is allowed.
|
|
64
|
+
*/
|
|
65
|
+
isChatAllowed(chatId) {
|
|
66
|
+
// If no allowed chat IDs specified, allow all
|
|
67
|
+
if (!this.settings.allowedChatIds || this.settings.allowedChatIds.length === 0) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return this.settings.allowedChatIds.includes(chatId);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Start long polling for updates.
|
|
74
|
+
*/
|
|
75
|
+
async poll(onCommand, onMessage) {
|
|
76
|
+
if (!this.polling)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
const updates = await this.apiRequest("getUpdates", {
|
|
80
|
+
offset: this.lastUpdateId + 1,
|
|
81
|
+
timeout: 30, // Long polling timeout in seconds
|
|
82
|
+
});
|
|
83
|
+
for (const update of updates) {
|
|
84
|
+
this.lastUpdateId = update.update_id;
|
|
85
|
+
if (update.message?.text) {
|
|
86
|
+
const chatId = String(update.message.chat.id);
|
|
87
|
+
// Check if chat is allowed
|
|
88
|
+
if (!this.isChatAllowed(chatId)) {
|
|
89
|
+
if (this.debug) {
|
|
90
|
+
console.log(`[telegram] Ignoring message from unauthorized chat: ${chatId}`);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const message = {
|
|
95
|
+
text: update.message.text,
|
|
96
|
+
chatId,
|
|
97
|
+
senderId: update.message.from ? String(update.message.from.id) : undefined,
|
|
98
|
+
senderName: update.message.from
|
|
99
|
+
? [update.message.from.first_name, update.message.from.last_name].filter(Boolean).join(" ")
|
|
100
|
+
: undefined,
|
|
101
|
+
timestamp: new Date(update.message.date * 1000),
|
|
102
|
+
raw: update,
|
|
103
|
+
};
|
|
104
|
+
// Call raw message handler if provided
|
|
105
|
+
if (onMessage) {
|
|
106
|
+
try {
|
|
107
|
+
await onMessage(message);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (this.debug) {
|
|
111
|
+
console.error(`[telegram] Message handler error: ${err}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Try to parse as a command
|
|
116
|
+
const command = parseCommand(message.text, message);
|
|
117
|
+
if (command) {
|
|
118
|
+
try {
|
|
119
|
+
await onCommand(command);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (this.debug) {
|
|
123
|
+
console.error(`[telegram] Command handler error: ${err}`);
|
|
124
|
+
}
|
|
125
|
+
// Send error message to chat
|
|
126
|
+
await this.sendMessage(chatId, `Error executing command: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (this.debug) {
|
|
134
|
+
console.error(`[telegram] Poll error: ${err}`);
|
|
135
|
+
}
|
|
136
|
+
// Wait a bit before retrying on error
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
138
|
+
}
|
|
139
|
+
// Schedule next poll
|
|
140
|
+
if (this.polling) {
|
|
141
|
+
this.pollingTimeout = setTimeout(() => this.poll(onCommand, onMessage), 100);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async connect(onCommand, onMessage) {
|
|
145
|
+
if (this.connected) {
|
|
146
|
+
throw new Error("Already connected");
|
|
147
|
+
}
|
|
148
|
+
// Verify bot token by calling getMe
|
|
149
|
+
try {
|
|
150
|
+
const me = await this.apiRequest("getMe");
|
|
151
|
+
if (this.debug) {
|
|
152
|
+
console.log(`[telegram] Connected as @${me.username || me.first_name} (ID: ${me.id})`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
throw new Error(`Failed to connect to Telegram: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
157
|
+
}
|
|
158
|
+
this.connected = true;
|
|
159
|
+
this.polling = true;
|
|
160
|
+
// Start polling
|
|
161
|
+
this.poll(onCommand, onMessage);
|
|
162
|
+
}
|
|
163
|
+
async sendMessage(chatId, text) {
|
|
164
|
+
if (!this.connected) {
|
|
165
|
+
throw new Error("Not connected");
|
|
166
|
+
}
|
|
167
|
+
await this.apiRequest("sendMessage", {
|
|
168
|
+
chat_id: chatId,
|
|
169
|
+
text,
|
|
170
|
+
parse_mode: "HTML",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async disconnect() {
|
|
174
|
+
this.polling = false;
|
|
175
|
+
this.connected = false;
|
|
176
|
+
if (this.pollingTimeout) {
|
|
177
|
+
clearTimeout(this.pollingTimeout);
|
|
178
|
+
this.pollingTimeout = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
isConnected() {
|
|
182
|
+
return this.connected;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Create a Telegram chat client from settings.
|
|
187
|
+
*/
|
|
188
|
+
export function createTelegramClient(settings, debug = false) {
|
|
189
|
+
return new TelegramChatClient(settings, debug);
|
|
190
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat client interface for communicating with external chat services.
|
|
3
|
+
* This provides a unified interface for different chat providers (Telegram, Discord, Slack, etc.)
|
|
4
|
+
*/
|
|
5
|
+
export interface ChatMessage {
|
|
6
|
+
/** The text content of the message */
|
|
7
|
+
text: string;
|
|
8
|
+
/** The chat/channel ID the message was sent in */
|
|
9
|
+
chatId: string;
|
|
10
|
+
/** The sender's ID (user or bot) */
|
|
11
|
+
senderId?: string;
|
|
12
|
+
/** The sender's display name */
|
|
13
|
+
senderName?: string;
|
|
14
|
+
/** Timestamp when the message was received */
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
/** Raw message data from the provider */
|
|
17
|
+
raw?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface ChatCommand {
|
|
20
|
+
/** The project ID extracted from the message (e.g., "abc" from "abc run") */
|
|
21
|
+
projectId: string;
|
|
22
|
+
/** The command name (e.g., "run", "status", "add", "exec") */
|
|
23
|
+
command: string;
|
|
24
|
+
/** Arguments passed to the command */
|
|
25
|
+
args: string[];
|
|
26
|
+
/** The original message */
|
|
27
|
+
message: ChatMessage;
|
|
28
|
+
}
|
|
29
|
+
export interface ChatClientConfig {
|
|
30
|
+
/** Whether the chat client is enabled */
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
/** The chat provider type (e.g., "telegram") */
|
|
33
|
+
provider: "telegram";
|
|
34
|
+
/** Provider-specific settings */
|
|
35
|
+
settings: TelegramSettings;
|
|
36
|
+
}
|
|
37
|
+
export interface TelegramSettings {
|
|
38
|
+
/** Telegram Bot API token */
|
|
39
|
+
botToken: string;
|
|
40
|
+
/** Allowed chat IDs (for security - only respond in these chats) */
|
|
41
|
+
allowedChatIds?: string[];
|
|
42
|
+
}
|
|
43
|
+
export type ChatProvider = "telegram";
|
|
44
|
+
/**
|
|
45
|
+
* Callback for handling incoming chat commands.
|
|
46
|
+
*/
|
|
47
|
+
export type ChatCommandHandler = (command: ChatCommand) => Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Callback for handling raw messages (for custom processing).
|
|
50
|
+
*/
|
|
51
|
+
export type ChatMessageHandler = (message: ChatMessage) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Abstract interface for chat clients.
|
|
54
|
+
* Implementations should handle provider-specific API calls.
|
|
55
|
+
*/
|
|
56
|
+
export interface ChatClient {
|
|
57
|
+
/** The provider type */
|
|
58
|
+
readonly provider: ChatProvider;
|
|
59
|
+
/**
|
|
60
|
+
* Connect to the chat service and start listening for messages.
|
|
61
|
+
* @param onCommand Callback for parsed commands (e.g., "abc run")
|
|
62
|
+
* @param onMessage Optional callback for all messages
|
|
63
|
+
*/
|
|
64
|
+
connect(onCommand: ChatCommandHandler, onMessage?: ChatMessageHandler): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Send a text message to a specific chat.
|
|
67
|
+
* @param chatId The chat ID to send to
|
|
68
|
+
* @param text The message text
|
|
69
|
+
*/
|
|
70
|
+
sendMessage(chatId: string, text: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Disconnect from the chat service.
|
|
73
|
+
*/
|
|
74
|
+
disconnect(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Check if the client is currently connected.
|
|
77
|
+
*/
|
|
78
|
+
isConnected(): boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Project registration for chat commands.
|
|
82
|
+
* Maps project IDs to their configurations.
|
|
83
|
+
*/
|
|
84
|
+
export interface ChatProjectRegistration {
|
|
85
|
+
/** Short project ID (3-digit identifier) */
|
|
86
|
+
projectId: string;
|
|
87
|
+
/** Full project name */
|
|
88
|
+
projectName: string;
|
|
89
|
+
/** Path to the project's .ralph directory */
|
|
90
|
+
ralphDir: string;
|
|
91
|
+
/** The chat ID where this project was registered */
|
|
92
|
+
chatId: string;
|
|
93
|
+
/** Timestamp when registered */
|
|
94
|
+
registeredAt: Date;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Generate a short project ID (3 alphanumeric characters).
|
|
98
|
+
* Used to identify projects in chat commands.
|
|
99
|
+
*/
|
|
100
|
+
export declare function generateProjectId(): string;
|
|
101
|
+
/**
|
|
102
|
+
* Parse a chat message to extract a command.
|
|
103
|
+
*
|
|
104
|
+
* Supports slash commands (preferred):
|
|
105
|
+
* - "/run" -> { command: "run", args: [] }
|
|
106
|
+
* - "/status" -> { command: "status", args: [] }
|
|
107
|
+
* - "/exec npm test" -> { command: "exec", args: ["npm", "test"] }
|
|
108
|
+
* - "/add Fix the login bug" -> { command: "add", args: ["Fix", "the", "login", "bug"] }
|
|
109
|
+
*/
|
|
110
|
+
export declare function parseCommand(text: string, message: ChatMessage): ChatCommand | null;
|
|
111
|
+
/**
|
|
112
|
+
* Format a status message for a project.
|
|
113
|
+
*/
|
|
114
|
+
export declare function formatStatusMessage(projectName: string, status: "running" | "idle" | "completed" | "error", details?: string): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat client interface for communicating with external chat services.
|
|
3
|
+
* This provides a unified interface for different chat providers (Telegram, Discord, Slack, etc.)
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generate a short project ID (3 alphanumeric characters).
|
|
7
|
+
* Used to identify projects in chat commands.
|
|
8
|
+
*/
|
|
9
|
+
export function generateProjectId() {
|
|
10
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
11
|
+
let id = "";
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
14
|
+
}
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse a chat message to extract a command.
|
|
19
|
+
*
|
|
20
|
+
* Supports slash commands (preferred):
|
|
21
|
+
* - "/run" -> { command: "run", args: [] }
|
|
22
|
+
* - "/status" -> { command: "status", args: [] }
|
|
23
|
+
* - "/exec npm test" -> { command: "exec", args: ["npm", "test"] }
|
|
24
|
+
* - "/add Fix the login bug" -> { command: "add", args: ["Fix", "the", "login", "bug"] }
|
|
25
|
+
*/
|
|
26
|
+
export function parseCommand(text, message) {
|
|
27
|
+
const trimmed = text.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
return null;
|
|
30
|
+
// Valid commands
|
|
31
|
+
const validCommands = ["run", "status", "add", "exec", "stop", "help", "start", "action"];
|
|
32
|
+
// Check for slash command format: /command [args...]
|
|
33
|
+
if (trimmed.startsWith("/")) {
|
|
34
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
35
|
+
if (parts.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
const [command, ...args] = parts;
|
|
38
|
+
const cmd = command.toLowerCase();
|
|
39
|
+
// Handle Telegram's /start command specially
|
|
40
|
+
if (cmd === "start") {
|
|
41
|
+
return {
|
|
42
|
+
projectId: "",
|
|
43
|
+
command: "help",
|
|
44
|
+
args: [],
|
|
45
|
+
message,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!validCommands.includes(cmd)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
projectId: "",
|
|
53
|
+
command: cmd,
|
|
54
|
+
args,
|
|
55
|
+
message,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Format a status message for a project.
|
|
62
|
+
*/
|
|
63
|
+
export function formatStatusMessage(projectName, status, details) {
|
|
64
|
+
const statusIcons = {
|
|
65
|
+
running: "[...]",
|
|
66
|
+
idle: "[_]",
|
|
67
|
+
completed: "[OK]",
|
|
68
|
+
error: "[X]",
|
|
69
|
+
};
|
|
70
|
+
const icon = statusIcons[status] || "[?]";
|
|
71
|
+
let message = `${icon} ${projectName}: ${status}`;
|
|
72
|
+
if (details) {
|
|
73
|
+
message += `\n${details}`;
|
|
74
|
+
}
|
|
75
|
+
return message;
|
|
76
|
+
}
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -27,12 +27,55 @@ export interface AsciinemaConfig {
|
|
|
27
27
|
outputDir?: string;
|
|
28
28
|
streamJson?: StreamJsonConfig;
|
|
29
29
|
}
|
|
30
|
+
export interface DaemonActionConfig {
|
|
31
|
+
command: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface NtfyNotificationConfig {
|
|
35
|
+
topic: string;
|
|
36
|
+
server?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface NotificationsConfig {
|
|
39
|
+
provider: "ntfy" | "command";
|
|
40
|
+
ntfy?: NtfyNotificationConfig;
|
|
41
|
+
command?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Event types that can trigger daemon calls.
|
|
45
|
+
* - task_complete: Fired after each task is completed
|
|
46
|
+
* - ralph_complete: Fired when ralph finishes all work (PRD complete)
|
|
47
|
+
* - iteration_complete: Fired after each iteration
|
|
48
|
+
* - error: Fired when an error occurs
|
|
49
|
+
*/
|
|
50
|
+
export type DaemonEventType = "task_complete" | "ralph_complete" | "iteration_complete" | "error";
|
|
51
|
+
export interface DaemonEventConfig {
|
|
52
|
+
action: string;
|
|
53
|
+
args?: string[];
|
|
54
|
+
message?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface DaemonConfig {
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
socketPath?: string;
|
|
59
|
+
actions?: Record<string, DaemonActionConfig>;
|
|
60
|
+
events?: Partial<Record<DaemonEventType, DaemonEventConfig[]>>;
|
|
61
|
+
}
|
|
62
|
+
export interface TelegramChatSettings {
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
botToken: string;
|
|
65
|
+
allowedChatIds?: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface ChatConfig {
|
|
68
|
+
enabled?: boolean;
|
|
69
|
+
provider?: "telegram";
|
|
70
|
+
telegram?: TelegramChatSettings;
|
|
71
|
+
}
|
|
30
72
|
export interface RalphConfig {
|
|
31
73
|
language: string;
|
|
32
74
|
checkCommand: string;
|
|
33
75
|
testCommand: string;
|
|
34
76
|
imageName?: string;
|
|
35
77
|
notifyCommand?: string;
|
|
78
|
+
notifications?: NotificationsConfig;
|
|
36
79
|
technologies?: string[];
|
|
37
80
|
javaVersion?: number;
|
|
38
81
|
cli?: CliConfig;
|
|
@@ -55,11 +98,15 @@ export interface RalphConfig {
|
|
|
55
98
|
firewall?: {
|
|
56
99
|
allowedDomains?: string[];
|
|
57
100
|
};
|
|
101
|
+
autoStart?: boolean;
|
|
102
|
+
restartCount?: number;
|
|
58
103
|
};
|
|
59
104
|
claude?: {
|
|
60
105
|
mcpServers?: Record<string, McpServerConfig>;
|
|
61
106
|
skills?: SkillConfig[];
|
|
62
107
|
};
|
|
108
|
+
daemon?: DaemonConfig;
|
|
109
|
+
chat?: ChatConfig;
|
|
63
110
|
}
|
|
64
111
|
export declare const DEFAULT_CLI_CONFIG: CliConfig;
|
|
65
112
|
export declare function getCliConfig(config: RalphConfig): CliConfig;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface DaemonRequest {
|
|
2
|
+
action: string;
|
|
3
|
+
args?: string[];
|
|
4
|
+
}
|
|
5
|
+
export interface DaemonResponse {
|
|
6
|
+
success: boolean;
|
|
7
|
+
message?: string;
|
|
8
|
+
output?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get the appropriate messages path based on whether we're in a container or on host.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getDaemonSocketPath(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Check if the daemon is available (messages file can be written).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isDaemonAvailable(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Send a request to the daemon and get a response.
|
|
21
|
+
*
|
|
22
|
+
* @param action The action to execute
|
|
23
|
+
* @param args Optional arguments to pass to the action
|
|
24
|
+
* @param timeout Timeout in milliseconds (default: 10000)
|
|
25
|
+
*/
|
|
26
|
+
export declare function sendDaemonRequest(action: string, args?: string[], timeout?: number): Promise<DaemonResponse>;
|
|
27
|
+
/**
|
|
28
|
+
* Send a ping to check if daemon is responsive.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pingDaemon(): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Send a notification through the daemon.
|
|
33
|
+
*
|
|
34
|
+
* @param message The notification message
|
|
35
|
+
*/
|
|
36
|
+
export declare function sendDaemonNotification(message: string): Promise<DaemonResponse>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { isRunningInContainer } from "./config.js";
|
|
3
|
+
import { getMessagesPath, sendMessage, waitForResponse, } from "./message-queue.js";
|
|
4
|
+
/**
|
|
5
|
+
* Get the appropriate messages path based on whether we're in a container or on host.
|
|
6
|
+
*/
|
|
7
|
+
export function getDaemonSocketPath() {
|
|
8
|
+
// For backwards compatibility, return the messages path
|
|
9
|
+
return getMessagesPath(isRunningInContainer());
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check if the daemon is available (messages file can be written).
|
|
13
|
+
*/
|
|
14
|
+
export function isDaemonAvailable() {
|
|
15
|
+
const messagesPath = getMessagesPath(isRunningInContainer());
|
|
16
|
+
// In container, check if the .ralph directory is mounted
|
|
17
|
+
if (isRunningInContainer()) {
|
|
18
|
+
return existsSync("/workspace/.ralph");
|
|
19
|
+
}
|
|
20
|
+
// On host, check if .ralph directory exists
|
|
21
|
+
return existsSync(messagesPath.replace("/messages.json", ""));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Send a request to the daemon and get a response.
|
|
25
|
+
*
|
|
26
|
+
* @param action The action to execute
|
|
27
|
+
* @param args Optional arguments to pass to the action
|
|
28
|
+
* @param timeout Timeout in milliseconds (default: 10000)
|
|
29
|
+
*/
|
|
30
|
+
export async function sendDaemonRequest(action, args, timeout = 10000) {
|
|
31
|
+
const messagesPath = getMessagesPath(isRunningInContainer());
|
|
32
|
+
try {
|
|
33
|
+
// Send message via file queue
|
|
34
|
+
const messageId = sendMessage(messagesPath, "sandbox", action, args);
|
|
35
|
+
// Wait for response
|
|
36
|
+
const response = await waitForResponse(messagesPath, messageId, timeout);
|
|
37
|
+
if (!response) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: `Request timed out after ${timeout}ms. Make sure the daemon is running on the host.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
success: response.success,
|
|
45
|
+
output: response.output,
|
|
46
|
+
error: response.error,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: `Failed to send message: ${err instanceof Error ? err.message : "unknown error"}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Send a ping to check if daemon is responsive.
|
|
58
|
+
*/
|
|
59
|
+
export async function pingDaemon() {
|
|
60
|
+
const response = await sendDaemonRequest("ping", [], 5000);
|
|
61
|
+
return response.success && response.output === "pong";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Send a notification through the daemon.
|
|
65
|
+
*
|
|
66
|
+
* @param message The notification message
|
|
67
|
+
*/
|
|
68
|
+
export async function sendDaemonNotification(message) {
|
|
69
|
+
return sendDaemonRequest("notify", [message]);
|
|
70
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface Message {
|
|
2
|
+
id: string;
|
|
3
|
+
from: "sandbox" | "host";
|
|
4
|
+
action: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
timestamp: number;
|
|
7
|
+
status: "pending" | "done";
|
|
8
|
+
response?: {
|
|
9
|
+
success: boolean;
|
|
10
|
+
output?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get the path to the messages file.
|
|
16
|
+
* Uses /workspace/.ralph in container, .ralph in host.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getMessagesPath(inContainer: boolean): string;
|
|
19
|
+
/**
|
|
20
|
+
* Read all messages from the queue.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readMessages(messagesPath: string): Message[];
|
|
23
|
+
/**
|
|
24
|
+
* Write messages to the queue (atomic write).
|
|
25
|
+
*/
|
|
26
|
+
export declare function writeMessages(messagesPath: string, messages: Message[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the messages file with a daemon_started message.
|
|
29
|
+
* Called when the daemon starts.
|
|
30
|
+
*/
|
|
31
|
+
export declare function initializeMessages(messagesPath: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Add a message to the queue.
|
|
34
|
+
* Returns the message ID.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sendMessage(messagesPath: string, from: "sandbox" | "host", action: string, args?: string[]): string;
|
|
37
|
+
export type MessageResponse = {
|
|
38
|
+
success: boolean;
|
|
39
|
+
output?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Wait for a response to a message.
|
|
44
|
+
* Returns the response or null if timeout.
|
|
45
|
+
*/
|
|
46
|
+
export declare function waitForResponse(messagesPath: string, messageId: string, timeout?: number, pollInterval?: number): Promise<MessageResponse | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Get pending messages for a recipient.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getPendingMessages(messagesPath: string, from: "sandbox" | "host"): Message[];
|
|
51
|
+
/**
|
|
52
|
+
* Mark a message as done with a response.
|
|
53
|
+
*/
|
|
54
|
+
export declare function respondToMessage(messagesPath: string, messageId: string, response: MessageResponse): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Clean up old messages (older than maxAge milliseconds).
|
|
57
|
+
*/
|
|
58
|
+
export declare function cleanupOldMessages(messagesPath: string, maxAge?: number): number;
|