ralph-cli-sandboxed 0.2.7 → 0.2.8

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 CHANGED
@@ -83,6 +83,57 @@ After running `ralph init`, you'll have:
83
83
  └── ...
84
84
  ```
85
85
 
86
+ ### Notifications
87
+
88
+ Ralph can send notifications when events occur during automation. Configure a notification command in `.ralph/config.json`:
89
+
90
+ ```json
91
+ {
92
+ "notifyCommand": "ntfy pub mytopic"
93
+ }
94
+ ```
95
+
96
+ The message is appended as the last argument to your command. Supported notification tools include:
97
+
98
+ | Tool | Example Command | Description |
99
+ |------|----------------|-------------|
100
+ | [ntfy](https://ntfy.sh/) | `ntfy pub mytopic` | Push notifications to phone/desktop |
101
+ | notify-send (Linux) | `notify-send Ralph` | Desktop notifications on Linux |
102
+ | terminal-notifier (macOS) | `terminal-notifier -title Ralph -message` | Desktop notifications on macOS |
103
+ | Custom script | `/path/to/notify.sh` | Your own notification script |
104
+
105
+ #### Notification Events
106
+
107
+ Ralph sends notifications for these events:
108
+
109
+ | Event | Message | When |
110
+ |-------|---------|------|
111
+ | PRD Complete | "Ralph: PRD Complete! All tasks finished." | All PRD tasks are marked as passing |
112
+ | Iteration Complete | "Ralph: Iteration complete." | Single `ralph once` iteration finishes |
113
+ | Run Stopped | "Ralph: Run stopped..." | `ralph run` stops due to no progress or max failures |
114
+ | Error | "Ralph: An error occurred." | CLI fails repeatedly |
115
+
116
+ #### Example: ntfy Setup
117
+
118
+ [ntfy](https://ntfy.sh/) is a simple HTTP-based pub-sub notification service:
119
+
120
+ ```bash
121
+ # 1. Install ntfy CLI
122
+ pip install ntfy
123
+
124
+ # 2. Subscribe to your topic on your phone (ntfy app) or browser
125
+ # Visit: https://ntfy.sh/your-unique-topic
126
+
127
+ # 3. Configure ralph
128
+ # In .ralph/config.json:
129
+ {
130
+ "notifyCommand": "ntfy pub your-unique-topic"
131
+ }
132
+
133
+ # 4. Run ralph - you'll get notifications on completion
134
+ ralph docker run
135
+ ```
136
+
86
137
  ### Supported Languages
87
138
 
88
139
  Ralph supports 18 programming languages with pre-configured build/test commands:
@@ -116,8 +167,9 @@ Ralph supports multiple AI CLI tools. Select your provider during `ralph init`:
116
167
  |-----|--------|----------------------|-------|
117
168
  | [Claude Code](https://github.com/anthropics/claude-code) | Working | `ANTHROPIC_API_KEY` | Default provider. Also supports ~/.claude OAuth credentials |
118
169
  | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Working | `GEMINI_API_KEY`, `GOOGLE_API_KEY` | |
119
- | [OpenCode](https://github.com/anomalyco/opencode) | Working | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` | Requires [PR #9073](https://github.com/anomalyco/opencode/pull/9073) |
170
+ | [OpenCode](https://github.com/anomalyco/opencode) | Working | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` | No autonomous/yolo mode yet. Requires [PR #9073](https://github.com/anomalyco/opencode/pull/9073) |
120
171
  | [Aider](https://github.com/paul-gauthier/aider) | Working | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | |
172
+ | [Goose](https://github.com/block/goose) | Working | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | Block's AI coding agent |
121
173
  | [Codex CLI](https://github.com/openai/codex) | Testers wanted | `OPENAI_API_KEY` | Sponsors welcome |
122
174
  | [AMP](https://ampcode.com/) | Testers wanted | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | Sponsors welcome |
123
175
  | Custom | - | User-defined | Configure your own CLI |
@@ -130,7 +182,7 @@ Ralph can be configured to use different AI CLI tools. By default, it uses Claud
130
182
  {
131
183
  "cli": {
132
184
  "command": "claude",
133
- "args": ["--permission-mode", "acceptEdits"],
185
+ "args": [],
134
186
  "modelArgs": ["--model"],
135
187
  "promptArgs": ["-p"]
136
188
  }
@@ -144,6 +196,102 @@ Ralph can be configured to use different AI CLI tools. By default, it uses Claud
144
196
 
145
197
  The prompt content and `--dangerously-skip-permissions` (in containers) are added automatically at runtime.
146
198
 
199
+ ### Stream-JSON Output
200
+
201
+ Ralph supports stream-json output mode for real-time streaming of AI responses. This feature provides cleaner terminal output and enables recording of raw JSON logs for debugging or replay.
202
+
203
+ #### Enabling Stream-JSON
204
+
205
+ Configure stream-json in `.ralph/config.json`:
206
+
207
+ ```json
208
+ {
209
+ "docker": {
210
+ "asciinema": {
211
+ "enabled": true,
212
+ "autoRecord": true,
213
+ "outputDir": ".recordings",
214
+ "streamJson": {
215
+ "enabled": true,
216
+ "saveRawJson": true
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ Configuration options:
224
+ - `enabled`: Enable stream-json output mode (default: `false`)
225
+ - `saveRawJson`: Save raw JSON output to `.jsonl` files (default: `true` when enabled)
226
+ - `outputDir`: Directory for recordings and logs (default: `.recordings`)
227
+
228
+ #### Provider Compatibility
229
+
230
+ Not all CLI providers support stream-json output. Here's the compatibility matrix:
231
+
232
+ | CLI Provider | Stream-JSON Support | Arguments Used |
233
+ |--------------|---------------------|----------------|
234
+ | Claude Code | ✅ Yes | `--output-format stream-json --verbose --print` |
235
+ | Gemini CLI | ✅ Yes | `--output-format json` |
236
+ | OpenCode | ✅ Yes | `--format json` |
237
+ | Codex CLI | ✅ Yes | `--json` |
238
+ | Goose | ✅ Yes | `--output-format stream-json` |
239
+ | Aider | ❌ No | - |
240
+ | AMP | ❌ No | - |
241
+ | Custom | ❌ No* | *Add `streamJsonArgs` to your custom config |
242
+
243
+ Each provider uses different command-line arguments and output formats. Ralph automatically selects the correct parser based on your configured provider.
244
+
245
+ #### Output Files
246
+
247
+ When stream-json is enabled, Ralph creates the following files in the `.recordings/` directory (configurable via `outputDir`):
248
+
249
+ | File Type | Pattern | Description |
250
+ |-----------|---------|-------------|
251
+ | `.jsonl` | `ralph-run-YYYYMMDD-HHMMSS.jsonl` | Raw JSON Lines log from `ralph run` |
252
+ | `.jsonl` | `ralph-once-YYYYMMDD-HHMMSS.jsonl` | Raw JSON Lines log from `ralph once` |
253
+ | `.cast` | `session-YYYYMMDD-HHMMSS.cast` | Asciinema terminal recording (when asciinema enabled) |
254
+
255
+ The `.jsonl` files contain one JSON object per line with the raw streaming events from the AI provider. These files are useful for:
256
+ - Debugging AI responses
257
+ - Replaying sessions
258
+ - Analyzing tool calls and outputs
259
+ - Building custom post-processing pipelines
260
+
261
+ #### Troubleshooting Stream-JSON
262
+
263
+ **Stream-JSON not working:**
264
+ 1. Verify your CLI provider supports stream-json (see compatibility matrix above)
265
+ 2. Check that `streamJson.enabled` is set to `true` in config
266
+ 3. Ensure your CLI provider is correctly installed and accessible
267
+
268
+ **No output appearing:**
269
+ - Stream-json parsing extracts human-readable text from JSON events
270
+ - Some providers emit different event types; Ralph handles the most common ones
271
+ - Use `--debug` flag with ralph commands to see raw parsing output: `[stream-json]` prefixed lines go to stderr
272
+
273
+ **Missing .jsonl files:**
274
+ - Verify `saveRawJson` is `true` (or not set, as it defaults to `true`)
275
+ - Check that the `outputDir` directory is writable
276
+ - Files are created at command start; check for permission errors
277
+
278
+ **Parser not recognizing events:**
279
+ - Each provider has a specific parser (ClaudeStreamParser, GeminiStreamParser, etc.)
280
+ - Unknown event types are handled by a default parser that extracts common fields
281
+ - If you see raw JSON in output, the parser may not support that event type yet
282
+
283
+ **Custom CLI provider:**
284
+ To add stream-json support for a custom CLI provider, add `streamJsonArgs` to your CLI config:
285
+ ```json
286
+ {
287
+ "cli": {
288
+ "command": "my-cli",
289
+ "promptArgs": ["-p"],
290
+ "streamJsonArgs": ["--json-output"]
291
+ }
292
+ }
293
+ ```
294
+
147
295
  ## PRD Format
148
296
 
149
297
  The PRD (`prd.json`) is an array of requirements:
@@ -181,11 +329,45 @@ File paths are resolved relative to the project root. Absolute paths are also su
181
329
 
182
330
  Ralph includes automatic PRD protection to handle cases where the LLM corrupts the PRD structure:
183
331
 
184
- - **Automatic backup**: Before each run, the PRD is backed up
332
+ - **Automatic backup**: Before each run, the PRD is backed up to `.ralph/backups/`
185
333
  - **Validation**: After each iteration, the PRD structure is validated
186
334
  - **Smart recovery**: If corrupted, ralph attempts to extract `passes: true` flags from the corrupted PRD and merge them into the backup
187
335
  - **Manual recovery**: Use `ralph fix-prd` to validate, auto-fix, or restore from a specific backup
188
336
 
337
+ ### When PRD Corruption Happens
338
+
339
+ LLMs sometimes modify the PRD file incorrectly, such as:
340
+ - Converting the array to an object
341
+ - Adding invalid JSON syntax
342
+ - Changing the structure entirely
343
+
344
+ If you see an error like:
345
+ ```
346
+ Error: prd.json is corrupted - expected an array of items.
347
+ The file may have been modified incorrectly by an LLM.
348
+
349
+ Run ralph fix-prd to diagnose and repair the file.
350
+ ```
351
+
352
+ ### Using fix-prd
353
+
354
+ The `ralph fix-prd` command diagnoses and repairs corrupted PRD files:
355
+
356
+ ```bash
357
+ ralph fix-prd # Auto-diagnose and fix
358
+ ralph fix-prd --verify # Check structure without modifying
359
+ ralph fix-prd backup.json # Restore from a specific backup file
360
+ ```
361
+
362
+ **What fix-prd does:**
363
+ 1. Validates JSON syntax and structure
364
+ 2. Checks that all required fields exist (category, description, steps, passes)
365
+ 3. Attempts to recover `passes: true` flags from corrupted files
366
+ 4. Falls back to the most recent backup if recovery fails
367
+ 5. Creates a fresh template PRD as a last resort
368
+
369
+ **Backups are stored in:** `.ralph/backups/backup.prd.YYYY-MM-DD-HHMMSS.json`
370
+
189
371
  ### Dynamic Iteration Limits
190
372
 
191
373
  To prevent runaway loops, `ralph run` limits iterations to `incomplete_tasks + 3`. This limit adjusts dynamically if new tasks are added during execution.
@@ -93,7 +93,7 @@ CLI CONFIGURATION:
93
93
  {
94
94
  "cli": {
95
95
  "command": "claude",
96
- "args": ["--permission-mode", "acceptEdits"],
96
+ "args": [],
97
97
  "yoloArgs": ["--dangerously-skip-permissions"]
98
98
  },
99
99
  "cliProvider": "claude"
@@ -104,6 +104,7 @@ CLI CONFIGURATION:
104
104
  - aider: AI pair programming
105
105
  - codex: OpenAI Codex CLI
106
106
  - gemini: Google Gemini CLI
107
+ - goose: Block's Goose AI coding agent
107
108
  - opencode: Open source AI coding agent
108
109
  - amp: Sourcegraph AMP CLI
109
110
  - custom: Configure your own CLI
@@ -2,138 +2,9 @@ import { spawn } from "child_process";
2
2
  import { existsSync, appendFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
5
- import { resolvePromptVariables } from "../templates/prompts.js";
6
- /**
7
- * Parses a stream-json line and extracts displayable text.
8
- * Formats output similar to Claude Code's normal terminal display.
9
- */
10
- function parseStreamJsonLine(line, debug = false) {
11
- try {
12
- const json = JSON.parse(line);
13
- if (debug && json.type) {
14
- process.stderr.write(`[stream-json] type: ${json.type}\n`);
15
- }
16
- // Handle Claude Code CLI stream-json events
17
- const type = json.type;
18
- switch (type) {
19
- // === Text Content ===
20
- case "content_block_delta":
21
- // Incremental text updates - the main streaming content
22
- if (json.delta?.type === "text_delta") {
23
- return json.delta.text || "";
24
- }
25
- // Tool input being streamed
26
- if (json.delta?.type === "input_json_delta") {
27
- return ""; // Don't show partial JSON, wait for complete tool call
28
- }
29
- return json.delta?.text || "";
30
- case "text":
31
- return json.text || "";
32
- // === Tool Use ===
33
- case "content_block_start":
34
- if (json.content_block?.type === "tool_use") {
35
- const toolName = json.content_block?.name || "unknown";
36
- return `\n── Tool: ${toolName} ──\n`;
37
- }
38
- if (json.content_block?.type === "text") {
39
- return json.content_block?.text || "";
40
- }
41
- return "";
42
- case "content_block_stop":
43
- // End of a content block - add newline after tool use
44
- return "";
45
- // === Tool Results ===
46
- case "tool_result":
47
- const toolOutput = json.content || json.output || "";
48
- const truncated = typeof toolOutput === "string" && toolOutput.length > 500
49
- ? toolOutput.substring(0, 500) + "... (truncated)"
50
- : toolOutput;
51
- return `\n── Tool Result ──\n${typeof truncated === "string" ? truncated : JSON.stringify(truncated, null, 2)}\n`;
52
- // === Assistant Messages ===
53
- case "assistant":
54
- const contents = json.message?.content || json.content || [];
55
- let output = "";
56
- for (const block of contents) {
57
- if (block.type === "text") {
58
- output += block.text || "";
59
- }
60
- else if (block.type === "tool_use") {
61
- output += `\n── Tool: ${block.name} ──\n`;
62
- if (block.input) {
63
- output += JSON.stringify(block.input, null, 2) + "\n";
64
- }
65
- }
66
- }
67
- return output;
68
- case "message_start":
69
- // Beginning of a new message
70
- return "\n";
71
- case "message_delta":
72
- // Message completion info (stop_reason, usage)
73
- if (json.delta?.stop_reason) {
74
- return `\n[${json.delta.stop_reason}]\n`;
75
- }
76
- return "";
77
- case "message_stop":
78
- return "\n";
79
- // === System/User Events ===
80
- case "system":
81
- if (json.message) {
82
- return `[System] ${json.message}\n`;
83
- }
84
- return "";
85
- case "user":
86
- // User message echo - usually not needed to display
87
- return "";
88
- // === Results and Errors ===
89
- case "result":
90
- if (json.result !== undefined) {
91
- return `\n── Result ──\n${JSON.stringify(json.result, null, 2)}\n`;
92
- }
93
- return "";
94
- case "error":
95
- const errMsg = json.error?.message || JSON.stringify(json.error);
96
- return `\n[Error] ${errMsg}\n`;
97
- // === File Operations (Claude Code specific) ===
98
- case "file_edit":
99
- case "file_write":
100
- const filePath = json.path || json.file || "unknown";
101
- return `\n── Writing: ${filePath} ──\n`;
102
- case "file_read":
103
- const readPath = json.path || json.file || "unknown";
104
- return `── Reading: ${readPath} ──\n`;
105
- case "bash":
106
- case "command":
107
- const cmd = json.command || json.content || "";
108
- return `\n── Running: ${cmd} ──\n`;
109
- case "bash_output":
110
- case "command_output":
111
- const cmdOutput = json.output || json.content || "";
112
- return cmdOutput + "\n";
113
- default:
114
- // Fallback: check for common text fields
115
- if (json.text)
116
- return json.text;
117
- if (json.content && typeof json.content === "string")
118
- return json.content;
119
- if (json.message && typeof json.message === "string")
120
- return json.message;
121
- if (json.output && typeof json.output === "string")
122
- return json.output;
123
- if (debug) {
124
- process.stderr.write(`[stream-json] unhandled type: ${type}, keys: ${Object.keys(json).join(", ")}\n`);
125
- }
126
- return "";
127
- }
128
- }
129
- catch (e) {
130
- // Not valid JSON
131
- if (debug) {
132
- process.stderr.write(`[stream-json] parse error: ${e}\n`);
133
- }
134
- return "";
135
- }
136
- }
5
+ import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
6
+ import { getStreamJsonParser } from "../utils/stream-json.js";
7
+ import { sendNotification } from "../utils/notification.js";
137
8
  export async function once(args) {
138
9
  // Parse flags
139
10
  let debug = false;
@@ -170,6 +41,11 @@ export async function once(args) {
170
41
  const streamJsonEnabled = streamJsonConfig?.enabled ?? false;
171
42
  const saveRawJson = streamJsonConfig?.saveRawJson !== false; // default true
172
43
  const outputDir = config.docker?.asciinema?.outputDir || ".recordings";
44
+ // Get provider-specific streamJsonArgs (empty array if not defined)
45
+ // This allows providers without JSON streaming to still have output displayed
46
+ const providers = getCliProviders();
47
+ const providerConfig = config.cliProvider ? providers[config.cliProvider] : providers["claude"];
48
+ const streamJsonArgs = providerConfig?.streamJsonArgs ?? [];
173
49
  console.log("Starting single ralph iteration...");
174
50
  if (streamJsonEnabled) {
175
51
  console.log("Stream JSON output enabled - displaying formatted Claude output");
@@ -182,15 +58,30 @@ export async function once(args) {
182
58
  // Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
183
59
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
184
60
  const promptArgs = cliConfig.promptArgs ?? ["-p"];
185
- const promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
186
61
  const cliArgs = [
187
62
  ...(cliConfig.args ?? []),
188
63
  ...yoloArgs,
189
64
  ];
190
- // Add stream-json output format if enabled
65
+ // Build the prompt value based on whether fileArgs is configured
66
+ // fileArgs (e.g., ["--read"] for Aider) means files are passed as separate arguments
67
+ // Otherwise, use @file syntax embedded in the prompt (Claude Code style)
68
+ let promptValue;
69
+ if (cliConfig.fileArgs && cliConfig.fileArgs.length > 0) {
70
+ // Add files as separate arguments (e.g., --read prd.json --read progress.txt)
71
+ for (const fileArg of cliConfig.fileArgs) {
72
+ cliArgs.push(fileArg, paths.prd);
73
+ cliArgs.push(fileArg, paths.progress);
74
+ }
75
+ promptValue = prompt;
76
+ }
77
+ else {
78
+ // Use @file syntax embedded in the prompt
79
+ promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
80
+ }
81
+ // Add stream-json output format if enabled (using provider-specific args)
191
82
  let jsonLogPath;
192
83
  if (streamJsonEnabled) {
193
- cliArgs.push("--output-format", "stream-json", "--verbose", "--print");
84
+ cliArgs.push(...streamJsonArgs);
194
85
  // Setup JSON log file if saving raw JSON
195
86
  if (saveRawJson) {
196
87
  const fullOutputDir = join(process.cwd(), outputDir);
@@ -212,7 +103,12 @@ export async function once(args) {
212
103
  console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
213
104
  }
214
105
  }
106
+ // Create provider-specific stream-json parser
107
+ const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
108
+ // Notification options for this run
109
+ const notifyOptions = { command: config.notifyCommand, debug };
215
110
  return new Promise((resolve, reject) => {
111
+ let output = ""; // Accumulate output for PRD complete detection
216
112
  if (streamJsonEnabled) {
217
113
  // Stream JSON mode: capture stdout, parse JSON, display clean text
218
114
  let lineBuffer = "";
@@ -240,19 +136,21 @@ export async function once(args) {
240
136
  // Ignore write errors
241
137
  }
242
138
  }
243
- // Parse and display clean text
244
- const text = parseStreamJsonLine(trimmedLine, debug);
139
+ // Parse and display clean text using provider-specific parser
140
+ const text = streamJsonParser.parseStreamJsonLine(trimmedLine);
245
141
  if (text) {
246
142
  process.stdout.write(text);
143
+ output += text; // Accumulate for completion detection
247
144
  }
248
145
  }
249
146
  else {
250
147
  // Non-JSON line - display as-is (might be status messages, errors, etc.)
251
148
  process.stdout.write(trimmedLine + "\n");
149
+ output += trimmedLine + "\n";
252
150
  }
253
151
  }
254
152
  });
255
- proc.on("close", (code) => {
153
+ proc.on("close", async (code) => {
256
154
  // Process any remaining buffered content
257
155
  if (lineBuffer.trim()) {
258
156
  const trimmedLine = lineBuffer.trim();
@@ -265,20 +163,30 @@ export async function once(args) {
265
163
  // Ignore write errors
266
164
  }
267
165
  }
268
- const text = parseStreamJsonLine(trimmedLine, debug);
166
+ const text = streamJsonParser.parseStreamJsonLine(trimmedLine);
269
167
  if (text) {
270
168
  process.stdout.write(text);
169
+ output += text;
271
170
  }
272
171
  }
273
172
  else {
274
173
  // Non-JSON remaining content
275
174
  process.stdout.write(trimmedLine + "\n");
175
+ output += trimmedLine + "\n";
276
176
  }
277
177
  }
278
178
  // Ensure final newline
279
179
  process.stdout.write("\n");
180
+ // Send notification based on outcome
280
181
  if (code !== 0) {
281
182
  console.error(`\n${cliConfig.command} exited with code ${code}`);
183
+ await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
184
+ }
185
+ else if (output.includes("<promise>COMPLETE</promise>")) {
186
+ await sendNotification("prd_complete", undefined, notifyOptions);
187
+ }
188
+ else {
189
+ await sendNotification("iteration_complete", undefined, notifyOptions);
282
190
  }
283
191
  resolve();
284
192
  });
@@ -287,13 +195,26 @@ export async function once(args) {
287
195
  });
288
196
  }
289
197
  else {
290
- // Standard mode: pass through all I/O
198
+ // Standard mode: capture stdout while passing through
291
199
  const proc = spawn(cliConfig.command, cliArgs, {
292
- stdio: "inherit",
200
+ stdio: ["inherit", "pipe", "inherit"],
293
201
  });
294
- proc.on("close", (code) => {
202
+ proc.stdout.on("data", (data) => {
203
+ const chunk = data.toString();
204
+ output += chunk;
205
+ process.stdout.write(chunk);
206
+ });
207
+ proc.on("close", async (code) => {
208
+ // Send notification based on outcome
295
209
  if (code !== 0) {
296
210
  console.error(`\n${cliConfig.command} exited with code ${code}`);
211
+ await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
212
+ }
213
+ else if (output.includes("<promise>COMPLETE</promise>")) {
214
+ await sendNotification("prd_complete", undefined, notifyOptions);
215
+ }
216
+ else {
217
+ await sendNotification("iteration_complete", undefined, notifyOptions);
297
218
  }
298
219
  resolve();
299
220
  });