ralph-cli-sandboxed 0.3.0 → 0.4.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.
Files changed (55) hide show
  1. package/dist/commands/action.d.ts +7 -0
  2. package/dist/commands/action.js +276 -0
  3. package/dist/commands/chat.js +95 -7
  4. package/dist/commands/config.js +6 -18
  5. package/dist/commands/fix-config.d.ts +4 -0
  6. package/dist/commands/fix-config.js +388 -0
  7. package/dist/commands/help.js +17 -0
  8. package/dist/commands/init.js +89 -2
  9. package/dist/commands/listen.js +50 -9
  10. package/dist/commands/prd.js +2 -2
  11. package/dist/config/languages.json +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/providers/telegram.d.ts +6 -2
  14. package/dist/providers/telegram.js +68 -2
  15. package/dist/templates/macos-scripts.d.ts +42 -0
  16. package/dist/templates/macos-scripts.js +448 -0
  17. package/dist/tui/ConfigEditor.d.ts +7 -0
  18. package/dist/tui/ConfigEditor.js +313 -0
  19. package/dist/tui/components/ArrayEditor.d.ts +22 -0
  20. package/dist/tui/components/ArrayEditor.js +193 -0
  21. package/dist/tui/components/BooleanToggle.d.ts +19 -0
  22. package/dist/tui/components/BooleanToggle.js +43 -0
  23. package/dist/tui/components/EditorPanel.d.ts +50 -0
  24. package/dist/tui/components/EditorPanel.js +232 -0
  25. package/dist/tui/components/HelpPanel.d.ts +13 -0
  26. package/dist/tui/components/HelpPanel.js +69 -0
  27. package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
  28. package/dist/tui/components/JsonSnippetEditor.js +380 -0
  29. package/dist/tui/components/KeyValueEditor.d.ts +34 -0
  30. package/dist/tui/components/KeyValueEditor.js +261 -0
  31. package/dist/tui/components/ObjectEditor.d.ts +23 -0
  32. package/dist/tui/components/ObjectEditor.js +227 -0
  33. package/dist/tui/components/PresetSelector.d.ts +23 -0
  34. package/dist/tui/components/PresetSelector.js +58 -0
  35. package/dist/tui/components/Preview.d.ts +18 -0
  36. package/dist/tui/components/Preview.js +190 -0
  37. package/dist/tui/components/ScrollableContainer.d.ts +38 -0
  38. package/dist/tui/components/ScrollableContainer.js +77 -0
  39. package/dist/tui/components/SectionNav.d.ts +31 -0
  40. package/dist/tui/components/SectionNav.js +130 -0
  41. package/dist/tui/components/StringEditor.d.ts +21 -0
  42. package/dist/tui/components/StringEditor.js +29 -0
  43. package/dist/tui/hooks/useConfig.d.ts +16 -0
  44. package/dist/tui/hooks/useConfig.js +89 -0
  45. package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
  46. package/dist/tui/hooks/useTerminalSize.js +48 -0
  47. package/dist/tui/utils/presets.d.ts +52 -0
  48. package/dist/tui/utils/presets.js +191 -0
  49. package/dist/tui/utils/validation.d.ts +49 -0
  50. package/dist/tui/utils/validation.js +198 -0
  51. package/dist/utils/chat-client.d.ts +31 -1
  52. package/dist/utils/chat-client.js +27 -1
  53. package/dist/utils/config.d.ts +7 -2
  54. package/docs/MACOS-DEVELOPMENT.md +435 -0
  55. package/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useStdout } from "ink";
3
+ /**
4
+ * Default terminal size (standard VT100 terminal dimensions).
5
+ */
6
+ export const DEFAULT_TERMINAL_SIZE = {
7
+ columns: 80,
8
+ rows: 24,
9
+ };
10
+ /**
11
+ * Minimum terminal size for the config editor.
12
+ */
13
+ export const MIN_TERMINAL_SIZE = {
14
+ columns: 60,
15
+ rows: 16,
16
+ };
17
+ /**
18
+ * Hook that returns the current terminal size and updates on resize.
19
+ * Falls back to default dimensions if terminal size cannot be determined.
20
+ */
21
+ export function useTerminalSize() {
22
+ const { stdout } = useStdout();
23
+ const getSize = () => {
24
+ if (stdout && typeof stdout.columns === "number" && typeof stdout.rows === "number") {
25
+ return {
26
+ columns: Math.max(stdout.columns, MIN_TERMINAL_SIZE.columns),
27
+ rows: Math.max(stdout.rows, MIN_TERMINAL_SIZE.rows),
28
+ };
29
+ }
30
+ return DEFAULT_TERMINAL_SIZE;
31
+ };
32
+ const [size, setSize] = useState(getSize);
33
+ useEffect(() => {
34
+ const handleResize = () => {
35
+ setSize(getSize());
36
+ };
37
+ // Listen for terminal resize events
38
+ if (stdout && typeof stdout.on === "function") {
39
+ stdout.on("resize", handleResize);
40
+ return () => {
41
+ stdout.off("resize", handleResize);
42
+ };
43
+ }
44
+ return undefined;
45
+ }, [stdout]);
46
+ return size;
47
+ }
48
+ export default useTerminalSize;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Configuration presets for the config editor.
3
+ * Provides quick setup templates for common integrations.
4
+ */
5
+ import type { RalphConfig } from "../../utils/config.js";
6
+ /**
7
+ * A preset defines default values for a specific integration.
8
+ */
9
+ export interface ConfigPreset {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ /** Category this preset belongs to (e.g., "chat", "notifications") */
14
+ category: "chat" | "notifications";
15
+ /** Fields to apply from this preset (dot-notation paths to values) */
16
+ fields: Record<string, unknown>;
17
+ }
18
+ /**
19
+ * Chat provider presets for quick setup.
20
+ */
21
+ export declare const CHAT_PRESETS: ConfigPreset[];
22
+ /**
23
+ * Notification provider presets for quick setup.
24
+ */
25
+ export declare const NOTIFICATION_PRESETS: ConfigPreset[];
26
+ /**
27
+ * All available presets grouped by category.
28
+ */
29
+ export declare const ALL_PRESETS: ConfigPreset[];
30
+ /**
31
+ * Get presets for a specific category.
32
+ */
33
+ export declare function getPresetsForCategory(category: "chat" | "notifications"): ConfigPreset[];
34
+ /**
35
+ * Get presets available for a specific config section.
36
+ * Maps section IDs to preset categories.
37
+ */
38
+ export declare function getPresetsForSection(sectionId: string): ConfigPreset[];
39
+ /**
40
+ * Check if a section has available presets.
41
+ */
42
+ export declare function sectionHasPresets(sectionId: string): boolean;
43
+ /**
44
+ * Apply a preset to a config object (immutably).
45
+ * Returns a new config with the preset fields applied.
46
+ */
47
+ export declare function applyPreset(config: RalphConfig, preset: ConfigPreset): RalphConfig;
48
+ /**
49
+ * Detect if a preset is currently active based on config values.
50
+ * Returns the preset ID if detected, or null if no preset matches.
51
+ */
52
+ export declare function detectActivePreset(config: RalphConfig, sectionId: string): string | null;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Configuration presets for the config editor.
3
+ * Provides quick setup templates for common integrations.
4
+ */
5
+ /**
6
+ * Chat provider presets for quick setup.
7
+ */
8
+ export const CHAT_PRESETS = [
9
+ {
10
+ id: "telegram",
11
+ name: "Telegram",
12
+ description: "Telegram Bot API for chat control",
13
+ category: "chat",
14
+ fields: {
15
+ "chat.enabled": true,
16
+ "chat.provider": "telegram",
17
+ "chat.telegram": {
18
+ enabled: true,
19
+ botToken: "",
20
+ allowedChatIds: [],
21
+ },
22
+ },
23
+ },
24
+ {
25
+ id: "slack",
26
+ name: "Slack",
27
+ description: "Slack App for chat control (coming soon)",
28
+ category: "chat",
29
+ fields: {
30
+ "chat.enabled": true,
31
+ "chat.provider": "slack",
32
+ },
33
+ },
34
+ {
35
+ id: "discord",
36
+ name: "Discord",
37
+ description: "Discord Bot for chat control (coming soon)",
38
+ category: "chat",
39
+ fields: {
40
+ "chat.enabled": true,
41
+ "chat.provider": "discord",
42
+ },
43
+ },
44
+ ];
45
+ /**
46
+ * Notification provider presets for quick setup.
47
+ */
48
+ export const NOTIFICATION_PRESETS = [
49
+ {
50
+ id: "ntfy",
51
+ name: "ntfy",
52
+ description: "Simple HTTP-based pub-sub notifications",
53
+ category: "notifications",
54
+ fields: {
55
+ "notifications.provider": "ntfy",
56
+ "notifications.ntfy": {
57
+ topic: "",
58
+ server: "https://ntfy.sh",
59
+ },
60
+ },
61
+ },
62
+ {
63
+ id: "pushover",
64
+ name: "Pushover",
65
+ description: "Real-time notifications to iOS, Android, and Desktop",
66
+ category: "notifications",
67
+ fields: {
68
+ "notifications.provider": "pushover",
69
+ "notifications.pushover": {
70
+ user: "",
71
+ token: "",
72
+ },
73
+ },
74
+ },
75
+ {
76
+ id: "gotify",
77
+ name: "Gotify",
78
+ description: "Self-hosted push notification server",
79
+ category: "notifications",
80
+ fields: {
81
+ "notifications.provider": "gotify",
82
+ "notifications.gotify": {
83
+ server: "",
84
+ token: "",
85
+ },
86
+ },
87
+ },
88
+ {
89
+ id: "command",
90
+ name: "Custom Command",
91
+ description: "Execute a custom shell command for notifications",
92
+ category: "notifications",
93
+ fields: {
94
+ "notifications.provider": "command",
95
+ "notifications.command": "",
96
+ },
97
+ },
98
+ ];
99
+ /**
100
+ * All available presets grouped by category.
101
+ */
102
+ export const ALL_PRESETS = [...CHAT_PRESETS, ...NOTIFICATION_PRESETS];
103
+ /**
104
+ * Get presets for a specific category.
105
+ */
106
+ export function getPresetsForCategory(category) {
107
+ return ALL_PRESETS.filter((preset) => preset.category === category);
108
+ }
109
+ /**
110
+ * Get presets available for a specific config section.
111
+ * Maps section IDs to preset categories.
112
+ */
113
+ export function getPresetsForSection(sectionId) {
114
+ switch (sectionId) {
115
+ case "chat":
116
+ return CHAT_PRESETS;
117
+ case "notifications":
118
+ return NOTIFICATION_PRESETS;
119
+ default:
120
+ return [];
121
+ }
122
+ }
123
+ /**
124
+ * Check if a section has available presets.
125
+ */
126
+ export function sectionHasPresets(sectionId) {
127
+ return getPresetsForSection(sectionId).length > 0;
128
+ }
129
+ /**
130
+ * Apply a preset to a config object (immutably).
131
+ * Returns a new config with the preset fields applied.
132
+ */
133
+ export function applyPreset(config, preset) {
134
+ const result = JSON.parse(JSON.stringify(config));
135
+ for (const [path, value] of Object.entries(preset.fields)) {
136
+ setValueAtPath(result, path, value);
137
+ }
138
+ return result;
139
+ }
140
+ /**
141
+ * Set a value at a dot-notation path in an object (mutates the object).
142
+ */
143
+ function setValueAtPath(obj, path, value) {
144
+ const parts = path.split(".");
145
+ let current = obj;
146
+ for (let i = 0; i < parts.length - 1; i++) {
147
+ const part = parts[i];
148
+ if (current[part] === undefined || current[part] === null) {
149
+ current[part] = {};
150
+ }
151
+ current = current[part];
152
+ }
153
+ const lastPart = parts[parts.length - 1];
154
+ current[lastPart] = value;
155
+ }
156
+ /**
157
+ * Detect if a preset is currently active based on config values.
158
+ * Returns the preset ID if detected, or null if no preset matches.
159
+ */
160
+ export function detectActivePreset(config, sectionId) {
161
+ const presets = getPresetsForSection(sectionId);
162
+ for (const preset of presets) {
163
+ // Check if the key distinguishing field matches
164
+ const mainField = Object.keys(preset.fields)[0];
165
+ const mainValue = getValueAtPath(config, mainField);
166
+ const presetValue = preset.fields[mainField];
167
+ if (mainValue === presetValue) {
168
+ return preset.id;
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+ /**
174
+ * Get a value at a dot-notation path from an object.
175
+ */
176
+ function getValueAtPath(obj, path) {
177
+ const parts = path.split(".");
178
+ let current = obj;
179
+ for (const part of parts) {
180
+ if (current === null || current === undefined) {
181
+ return undefined;
182
+ }
183
+ if (typeof current === "object") {
184
+ current = current[part];
185
+ }
186
+ else {
187
+ return undefined;
188
+ }
189
+ }
190
+ return current;
191
+ }
@@ -0,0 +1,49 @@
1
+ import type { RalphConfig } from "../../utils/config.js";
2
+ /**
3
+ * Validation error for a specific field.
4
+ */
5
+ export interface ValidationError {
6
+ field: string;
7
+ message: string;
8
+ type: "required" | "format" | "pattern";
9
+ }
10
+ /**
11
+ * Result of validating a configuration.
12
+ */
13
+ export interface ValidationResult {
14
+ valid: boolean;
15
+ errors: ValidationError[];
16
+ }
17
+ /**
18
+ * Validate that a port string matches the expected format.
19
+ * @param port - Port mapping string (e.g., "3000:3000")
20
+ * @returns true if valid, false otherwise
21
+ */
22
+ export declare function validatePortFormat(port: string): boolean;
23
+ /**
24
+ * Check if a required field has a valid non-empty value.
25
+ * @param value - The field value to check
26
+ * @returns true if valid, false otherwise
27
+ */
28
+ export declare function isRequiredFieldValid(value: unknown): boolean;
29
+ /**
30
+ * Validate the entire configuration.
31
+ * @param config - The configuration to validate
32
+ * @returns ValidationResult with valid flag and any errors found
33
+ */
34
+ export declare function validateConfig(config: RalphConfig): ValidationResult;
35
+ /**
36
+ * Get validation errors for a specific field path.
37
+ * Useful for inline error display in the editor.
38
+ * @param errors - List of all validation errors
39
+ * @param fieldPath - The field path to check (e.g., "language", "docker.ports")
40
+ * @returns Array of errors matching the field path
41
+ */
42
+ export declare function getFieldErrors(errors: ValidationError[], fieldPath: string): ValidationError[];
43
+ /**
44
+ * Check if a field has any validation errors.
45
+ * @param errors - List of all validation errors
46
+ * @param fieldPath - The field path to check
47
+ * @returns true if the field has errors, false otherwise
48
+ */
49
+ export declare function hasFieldError(errors: ValidationError[], fieldPath: string): boolean;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Required fields that cannot be empty.
3
+ */
4
+ const REQUIRED_FIELDS = ["language", "checkCommand", "testCommand"];
5
+ /**
6
+ * Port format pattern: host_port:container_port (e.g., "3000:3000", "8080:80")
7
+ */
8
+ const PORT_PATTERN = /^\d+:\d+$/;
9
+ /**
10
+ * Get a value at a dot-notation path from an object.
11
+ */
12
+ function getValueAtPath(obj, path) {
13
+ const parts = path.split(".");
14
+ let current = obj;
15
+ for (const part of parts) {
16
+ if (current === null || current === undefined) {
17
+ return undefined;
18
+ }
19
+ if (typeof current === "object") {
20
+ current = current[part];
21
+ }
22
+ else {
23
+ return undefined;
24
+ }
25
+ }
26
+ return current;
27
+ }
28
+ /**
29
+ * Validate that a port string matches the expected format.
30
+ * @param port - Port mapping string (e.g., "3000:3000")
31
+ * @returns true if valid, false otherwise
32
+ */
33
+ export function validatePortFormat(port) {
34
+ return PORT_PATTERN.test(port);
35
+ }
36
+ /**
37
+ * Check if a required field has a valid non-empty value.
38
+ * @param value - The field value to check
39
+ * @returns true if valid, false otherwise
40
+ */
41
+ export function isRequiredFieldValid(value) {
42
+ if (value === null || value === undefined) {
43
+ return false;
44
+ }
45
+ if (typeof value === "string") {
46
+ return value.trim().length > 0;
47
+ }
48
+ return true;
49
+ }
50
+ /**
51
+ * Convert a field path to a human-readable label.
52
+ */
53
+ function pathToLabel(path) {
54
+ const parts = path.split(".");
55
+ const lastPart = parts[parts.length - 1];
56
+ return lastPart
57
+ .replace(/([A-Z])/g, " $1")
58
+ .replace(/^./, (str) => str.toUpperCase())
59
+ .trim();
60
+ }
61
+ /**
62
+ * Validate required fields in the configuration.
63
+ */
64
+ function validateRequiredFields(config) {
65
+ const errors = [];
66
+ for (const field of REQUIRED_FIELDS) {
67
+ const value = getValueAtPath(config, field);
68
+ if (!isRequiredFieldValid(value)) {
69
+ errors.push({
70
+ field,
71
+ message: `${pathToLabel(field)} is required and cannot be empty`,
72
+ type: "required",
73
+ });
74
+ }
75
+ }
76
+ return errors;
77
+ }
78
+ /**
79
+ * Validate port format for all docker ports.
80
+ */
81
+ function validateDockerPorts(config) {
82
+ const errors = [];
83
+ const ports = config.docker?.ports;
84
+ if (!ports || !Array.isArray(ports)) {
85
+ return errors;
86
+ }
87
+ for (let i = 0; i < ports.length; i++) {
88
+ const port = ports[i];
89
+ if (typeof port === "string" && !validatePortFormat(port)) {
90
+ errors.push({
91
+ field: `docker.ports[${i}]`,
92
+ message: `Invalid port format: "${port}". Expected format: "host:container" (e.g., "3000:3000")`,
93
+ type: "pattern",
94
+ });
95
+ }
96
+ }
97
+ return errors;
98
+ }
99
+ /**
100
+ * Required keys for each notification provider.
101
+ */
102
+ const NOTIFICATION_PROVIDER_REQUIRED_KEYS = {
103
+ ntfy: ["topic"],
104
+ pushover: ["user", "token"],
105
+ gotify: ["server", "token"],
106
+ };
107
+ /**
108
+ * Validate notification provider configuration.
109
+ * Checks that required keys are present for the selected provider.
110
+ */
111
+ function validateNotificationProvider(config) {
112
+ const errors = [];
113
+ const notifications = config.notifications;
114
+ if (!notifications || !notifications.provider) {
115
+ return errors;
116
+ }
117
+ const provider = notifications.provider;
118
+ // Skip validation for command provider (uses command string instead of key-value)
119
+ if (provider === "command") {
120
+ return errors;
121
+ }
122
+ const requiredKeys = NOTIFICATION_PROVIDER_REQUIRED_KEYS[provider];
123
+ if (!requiredKeys) {
124
+ return errors;
125
+ }
126
+ const providerConfig = notifications[provider];
127
+ if (!providerConfig || typeof providerConfig !== "object") {
128
+ // Provider config is missing entirely
129
+ for (const key of requiredKeys) {
130
+ errors.push({
131
+ field: `notifications.${provider}.${key}`,
132
+ message: `${key} is required for ${provider} provider`,
133
+ type: "required",
134
+ });
135
+ }
136
+ return errors;
137
+ }
138
+ // Check each required key
139
+ const configObj = providerConfig;
140
+ for (const key of requiredKeys) {
141
+ const value = configObj[key];
142
+ if (!isRequiredFieldValid(value)) {
143
+ errors.push({
144
+ field: `notifications.${provider}.${key}`,
145
+ message: `${key} is required for ${provider} provider`,
146
+ type: "required",
147
+ });
148
+ }
149
+ }
150
+ return errors;
151
+ }
152
+ /**
153
+ * Validate the entire configuration.
154
+ * @param config - The configuration to validate
155
+ * @returns ValidationResult with valid flag and any errors found
156
+ */
157
+ export function validateConfig(config) {
158
+ const errors = [];
159
+ // Check required fields
160
+ errors.push(...validateRequiredFields(config));
161
+ // Check docker port format
162
+ errors.push(...validateDockerPorts(config));
163
+ // Check notification provider required keys
164
+ errors.push(...validateNotificationProvider(config));
165
+ return {
166
+ valid: errors.length === 0,
167
+ errors,
168
+ };
169
+ }
170
+ /**
171
+ * Get validation errors for a specific field path.
172
+ * Useful for inline error display in the editor.
173
+ * @param errors - List of all validation errors
174
+ * @param fieldPath - The field path to check (e.g., "language", "docker.ports")
175
+ * @returns Array of errors matching the field path
176
+ */
177
+ export function getFieldErrors(errors, fieldPath) {
178
+ return errors.filter((error) => {
179
+ // Exact match
180
+ if (error.field === fieldPath) {
181
+ return true;
182
+ }
183
+ // Array field match (e.g., "docker.ports" matches "docker.ports[0]")
184
+ if (error.field.startsWith(fieldPath + "[")) {
185
+ return true;
186
+ }
187
+ return false;
188
+ });
189
+ }
190
+ /**
191
+ * Check if a field has any validation errors.
192
+ * @param errors - List of all validation errors
193
+ * @param fieldPath - The field path to check
194
+ * @returns true if the field has errors, false otherwise
195
+ */
196
+ export function hasFieldError(errors, fieldPath) {
197
+ return getFieldErrors(errors, fieldPath).length > 0;
198
+ }
@@ -49,6 +49,24 @@ export type ChatCommandHandler = (command: ChatCommand) => Promise<void>;
49
49
  * Callback for handling raw messages (for custom processing).
50
50
  */
51
51
  export type ChatMessageHandler = (message: ChatMessage) => Promise<void>;
52
+ /**
53
+ * Options for sending messages (provider-specific features).
54
+ */
55
+ export interface SendMessageOptions {
56
+ /** Inline keyboard buttons (Telegram-specific) */
57
+ inlineKeyboard?: InlineButton[][];
58
+ }
59
+ /**
60
+ * Inline button for chat messages.
61
+ */
62
+ export interface InlineButton {
63
+ /** Button text displayed to user */
64
+ text: string;
65
+ /** Callback data sent when button is pressed (used as command) */
66
+ callbackData?: string;
67
+ /** URL to open when button is pressed */
68
+ url?: string;
69
+ }
52
70
  /**
53
71
  * Abstract interface for chat clients.
54
72
  * Implementations should handle provider-specific API calls.
@@ -66,8 +84,9 @@ export interface ChatClient {
66
84
  * Send a text message to a specific chat.
67
85
  * @param chatId The chat ID to send to
68
86
  * @param text The message text
87
+ * @param options Optional message options (e.g., inline keyboard)
69
88
  */
70
- sendMessage(chatId: string, text: string): Promise<void>;
89
+ sendMessage(chatId: string, text: string, options?: SendMessageOptions): Promise<void>;
71
90
  /**
72
91
  * Disconnect from the chat service.
73
92
  */
@@ -108,6 +127,17 @@ export declare function generateProjectId(): string;
108
127
  * - "/add Fix the login bug" -> { command: "add", args: ["Fix", "the", "login", "bug"] }
109
128
  */
110
129
  export declare function parseCommand(text: string, message: ChatMessage): ChatCommand | null;
130
+ /**
131
+ * Strip ANSI escape codes from a string.
132
+ * This is useful for cleaning output before sending to chat services
133
+ * that don't support terminal formatting.
134
+ */
135
+ export declare function stripAnsiCodes(text: string): string;
136
+ /**
137
+ * Format status output for chat by stripping ANSI codes and removing
138
+ * progress bars that don't render well in chat.
139
+ */
140
+ export declare function formatStatusForChat(output: string): string;
111
141
  /**
112
142
  * Format a status message for a project.
113
143
  */
@@ -28,7 +28,7 @@ export function parseCommand(text, message) {
28
28
  if (!trimmed)
29
29
  return null;
30
30
  // Valid commands
31
- const validCommands = ["run", "status", "add", "exec", "stop", "help", "start", "action"];
31
+ const validCommands = ["run", "status", "add", "exec", "stop", "help", "start", "action", "claude"];
32
32
  // Check for slash command format: /command [args...]
33
33
  if (trimmed.startsWith("/")) {
34
34
  const parts = trimmed.slice(1).split(/\s+/);
@@ -57,6 +57,32 @@ export function parseCommand(text, message) {
57
57
  }
58
58
  return null;
59
59
  }
60
+ /**
61
+ * Strip ANSI escape codes from a string.
62
+ * This is useful for cleaning output before sending to chat services
63
+ * that don't support terminal formatting.
64
+ */
65
+ export function stripAnsiCodes(text) {
66
+ // Match ANSI escape sequences: ESC[...m (SGR), ESC[...K (EL), etc.
67
+ return text.replace(/\x1B\[[0-9;]*[mKJHfsu]/g, "");
68
+ }
69
+ /**
70
+ * Format status output for chat by stripping ANSI codes and removing
71
+ * progress bars that don't render well in chat.
72
+ */
73
+ export function formatStatusForChat(output) {
74
+ // Strip ANSI escape codes
75
+ let cleaned = stripAnsiCodes(output);
76
+ // Remove progress bar lines (lines with block characters ██░)
77
+ // These don't render well in chat clients
78
+ cleaned = cleaned
79
+ .split("\n")
80
+ .filter((line) => !line.includes("█") && !line.includes("░"))
81
+ .join("\n");
82
+ // Clean up extra blank lines that may result from removing progress bar
83
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
84
+ return cleaned.trim();
85
+ }
60
86
  /**
61
87
  * Format a status message for a project.
62
88
  */
@@ -31,13 +31,18 @@ export interface DaemonActionConfig {
31
31
  command: string;
32
32
  description?: string;
33
33
  }
34
+ export interface NotificationProviderConfig {
35
+ [key: string]: string | undefined;
36
+ }
34
37
  export interface NtfyNotificationConfig {
35
38
  topic: string;
36
39
  server?: string;
37
40
  }
38
41
  export interface NotificationsConfig {
39
- provider: "ntfy" | "command";
40
- ntfy?: NtfyNotificationConfig;
42
+ provider: "ntfy" | "pushover" | "gotify" | "command";
43
+ ntfy?: NotificationProviderConfig;
44
+ pushover?: NotificationProviderConfig;
45
+ gotify?: NotificationProviderConfig;
41
46
  command?: string;
42
47
  }
43
48
  /**