@hustle-together/api-dev-tools 3.12.3 → 3.12.16

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 (96) hide show
  1. package/.claude/commands/hustle-build.md +259 -0
  2. package/.claude/commands/hustle-combine.md +1089 -0
  3. package/.claude/commands/hustle-ui-create-page.md +1078 -0
  4. package/.claude/commands/hustle-ui-create.md +1058 -0
  5. package/.claude/hooks/auto-answer.py +305 -0
  6. package/.claude/hooks/cache-research.py +337 -0
  7. package/.claude/hooks/check-api-routes.py +168 -0
  8. package/.claude/hooks/check-playwright-setup.py +103 -0
  9. package/.claude/hooks/check-storybook-setup.py +81 -0
  10. package/.claude/hooks/check-update.py +132 -0
  11. package/.claude/hooks/completion-promise-detector.py +293 -0
  12. package/.claude/hooks/context-capacity-warning.py +171 -0
  13. package/.claude/hooks/detect-interruption.py +165 -0
  14. package/.claude/hooks/docs-update-check.py +120 -0
  15. package/.claude/hooks/enforce-a11y-audit.py +202 -0
  16. package/.claude/hooks/enforce-brand-guide.py +241 -0
  17. package/.claude/hooks/enforce-component-type-confirm.py +97 -0
  18. package/.claude/hooks/enforce-dry-run.py +134 -0
  19. package/.claude/hooks/enforce-freshness.py +184 -0
  20. package/.claude/hooks/enforce-page-components.py +186 -0
  21. package/.claude/hooks/enforce-page-data-schema.py +155 -0
  22. package/.claude/hooks/enforce-questions-sourced.py +146 -0
  23. package/.claude/hooks/enforce-schema-from-interview.py +248 -0
  24. package/.claude/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/.claude/hooks/enforce-ui-interview.py +130 -0
  26. package/.claude/hooks/generate-adr-options.py +282 -0
  27. package/.claude/hooks/generate-manifest-entry.py +1161 -0
  28. package/.claude/hooks/hook_utils.py +609 -0
  29. package/.claude/hooks/lib/__init__.py +1 -0
  30. package/.claude/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  31. package/.claude/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  32. package/.claude/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  33. package/.claude/hooks/lib/greptile.py +355 -0
  34. package/.claude/hooks/lib/ntfy.py +209 -0
  35. package/.claude/hooks/notify-input-needed.py +73 -0
  36. package/.claude/hooks/notify-phase-complete.py +90 -0
  37. package/.claude/hooks/ntfy-on-question.py +240 -0
  38. package/.claude/hooks/orchestrator-completion.py +313 -0
  39. package/.claude/hooks/orchestrator-handoff.py +267 -0
  40. package/.claude/hooks/orchestrator-session-startup.py +146 -0
  41. package/.claude/hooks/parallel-orchestrator.py +451 -0
  42. package/.claude/hooks/project-document-prompt.py +302 -0
  43. package/.claude/hooks/remote-question-proxy.py +284 -0
  44. package/.claude/hooks/remote-question-server.py +1224 -0
  45. package/.claude/hooks/run-code-review.py +393 -0
  46. package/.claude/hooks/run-visual-qa.py +338 -0
  47. package/.claude/hooks/session-logger.py +323 -0
  48. package/.claude/hooks/test-orchestrator-reground.py +248 -0
  49. package/.claude/hooks/track-scope-coverage.py +220 -0
  50. package/.claude/hooks/track-token-usage.py +121 -0
  51. package/.claude/hooks/update-adr-decision.py +236 -0
  52. package/.claude/hooks/update-api-showcase.py +161 -0
  53. package/.claude/hooks/update-registry.py +352 -0
  54. package/.claude/hooks/update-testing-checklist.py +195 -0
  55. package/.claude/hooks/update-ui-showcase.py +224 -0
  56. package/.claude/settings.local.json +7 -1
  57. package/.claude/test-auto-answer-bot.py +183 -0
  58. package/.claude/test-completion-detector.py +263 -0
  59. package/.claude/test-orchestrator-state.json +20 -0
  60. package/.claude/test-orchestrator.sh +271 -0
  61. package/.skills/api-create/SKILL.md +88 -3
  62. package/.skills/docs-sync/SKILL.md +260 -0
  63. package/.skills/hustle-build/SKILL.md +459 -0
  64. package/.skills/hustle-build-review/SKILL.md +518 -0
  65. package/CHANGELOG.md +87 -0
  66. package/README.md +86 -9
  67. package/bin/cli.js +1302 -88
  68. package/commands/hustle-api-create.md +22 -0
  69. package/commands/hustle-combine.md +81 -2
  70. package/commands/hustle-ui-create-page.md +84 -2
  71. package/commands/hustle-ui-create.md +82 -2
  72. package/hooks/auto-answer.py +228 -0
  73. package/hooks/check-update.py +132 -0
  74. package/hooks/ntfy-on-question.py +227 -0
  75. package/hooks/orchestrator-completion.py +313 -0
  76. package/hooks/orchestrator-handoff.py +189 -0
  77. package/hooks/orchestrator-session-startup.py +146 -0
  78. package/hooks/periodic-reground.py +230 -67
  79. package/hooks/update-api-showcase.py +13 -1
  80. package/hooks/update-ui-showcase.py +13 -1
  81. package/package.json +7 -3
  82. package/scripts/extract-schema-docs.cjs +322 -0
  83. package/templates/CLAUDE-SECTION.md +89 -64
  84. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  85. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  86. package/templates/api-showcase/_components/APITester.tsx +367 -58
  87. package/templates/docs/page.tsx +230 -0
  88. package/templates/hustle-build-defaults.json +84 -0
  89. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  90. package/templates/playwright-report/page.tsx +258 -0
  91. package/templates/settings.json +88 -7
  92. package/templates/test-results/page.tsx +237 -0
  93. package/templates/typedoc.json +19 -0
  94. package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
  95. package/templates/ui-showcase/page.tsx +1 -1
  96. package/.claude/api-dev-state.json +0 -466
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Project Document Prompt Hook
4
+
5
+ Prompts users for a project document (PRD, spec, deep research output) at the
6
+ start of /hustle-build. Stores the document in state for AI-powered decomposition.
7
+
8
+ Hook Type: PreToolUse (matcher: Skill)
9
+ Trigger: When /hustle-build is invoked
10
+ Version: 4.6.0
11
+
12
+ Flags:
13
+ --skip-document Skip the project document prompt
14
+ --from-document PATH Use specified file as project document
15
+ --no-document Alias for --skip-document
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+ from datetime import datetime
23
+
24
+ try:
25
+ from hook_utils import load_state, save_state, get_project_dir, log_workflow_event
26
+ UTILS_AVAILABLE = True
27
+ except ImportError:
28
+ UTILS_AVAILABLE = False
29
+
30
+
31
+ def get_project_dir_fallback():
32
+ """Get project directory from environment or current directory."""
33
+ return Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
34
+
35
+
36
+ def load_hustle_build_state():
37
+ """Load hustle-build orchestration state."""
38
+ project_dir = get_project_dir_fallback()
39
+ state_file = project_dir / ".claude" / "hustle-build-state.json"
40
+ if state_file.exists():
41
+ try:
42
+ return json.loads(state_file.read_text())
43
+ except Exception:
44
+ pass
45
+ return None
46
+
47
+
48
+ def save_hustle_build_state(state):
49
+ """Save hustle-build orchestration state."""
50
+ project_dir = get_project_dir_fallback()
51
+ state_file = project_dir / ".claude" / "hustle-build-state.json"
52
+ state_file.parent.mkdir(parents=True, exist_ok=True)
53
+ state_file.write_text(json.dumps(state, indent=2))
54
+
55
+
56
+ def parse_flags(args):
57
+ """Parse command-line style flags from arguments string."""
58
+ flags = {
59
+ "skip_document": False,
60
+ "from_document": None,
61
+ }
62
+
63
+ if not args:
64
+ return flags
65
+
66
+ # Check for skip flags
67
+ if "--skip-document" in args or "--no-document" in args:
68
+ flags["skip_document"] = True
69
+
70
+ # Check for --from-document PATH
71
+ if "--from-document" in args:
72
+ # Extract path after --from-document
73
+ parts = args.split("--from-document")
74
+ if len(parts) > 1:
75
+ path_part = parts[1].strip().split()[0] if parts[1].strip() else None
76
+ if path_part and not path_part.startswith("--"):
77
+ flags["from_document"] = path_part
78
+
79
+ return flags
80
+
81
+
82
+ def read_document_file(file_path):
83
+ """Read a document file and detect its format."""
84
+ path = Path(file_path)
85
+
86
+ if not path.exists():
87
+ # Try relative to project dir
88
+ project_dir = get_project_dir_fallback()
89
+ path = project_dir / file_path
90
+
91
+ if not path.exists():
92
+ return None, None, f"File not found: {file_path}"
93
+
94
+ try:
95
+ content = path.read_text()
96
+
97
+ # Detect format
98
+ suffix = path.suffix.lower()
99
+ if suffix in [".md", ".markdown"]:
100
+ fmt = "markdown"
101
+ elif suffix == ".json":
102
+ fmt = "json"
103
+ elif suffix in [".txt", ".text"]:
104
+ fmt = "text"
105
+ else:
106
+ # Guess based on content
107
+ if content.strip().startswith("{") or content.strip().startswith("["):
108
+ fmt = "json"
109
+ elif content.startswith("#") or "##" in content[:500]:
110
+ fmt = "markdown"
111
+ else:
112
+ fmt = "text"
113
+
114
+ return content, fmt, None
115
+ except Exception as e:
116
+ return None, None, f"Error reading file: {e}"
117
+
118
+
119
+ def main():
120
+ # Read tool input from stdin or environment
121
+ tool_input_raw = os.environ.get("CLAUDE_TOOL_INPUT", "")
122
+
123
+ # Also check stdin for hook input
124
+ try:
125
+ if not sys.stdin.isatty():
126
+ stdin_data = sys.stdin.read()
127
+ if stdin_data:
128
+ try:
129
+ hook_input = json.loads(stdin_data)
130
+ tool_input_raw = json.dumps(hook_input.get("tool_input", {}))
131
+ except json.JSONDecodeError:
132
+ pass
133
+ except Exception:
134
+ pass
135
+
136
+ try:
137
+ data = json.loads(tool_input_raw) if tool_input_raw else {}
138
+ skill_name = data.get("skill", "")
139
+ args = data.get("args", "")
140
+ except Exception:
141
+ # Not a skill invocation or invalid JSON
142
+ print(json.dumps({"continue": True}))
143
+ return
144
+
145
+ # Only trigger for hustle-build skill
146
+ if skill_name != "hustle-build":
147
+ print(json.dumps({"continue": True}))
148
+ return
149
+
150
+ # Parse flags from arguments
151
+ flags = parse_flags(args)
152
+
153
+ # Check for skip flag
154
+ if flags["skip_document"]:
155
+ print(json.dumps({"continue": True}))
156
+ return
157
+
158
+ # Check if project_spec already exists with content
159
+ state = load_hustle_build_state()
160
+ if state and state.get("project_spec", {}).get("raw_content"):
161
+ print(json.dumps({"continue": True}))
162
+ return
163
+
164
+ # Handle --from-document flag
165
+ if flags["from_document"]:
166
+ content, fmt, error = read_document_file(flags["from_document"])
167
+
168
+ if error:
169
+ # Inject error message
170
+ result = {
171
+ "continue": True,
172
+ "additionalContext": f"""
173
+ ## Project Document Error
174
+
175
+ Could not load document: {error}
176
+
177
+ Please provide the document path again or use `--skip-document` to proceed without a document.
178
+ """
179
+ }
180
+ print(json.dumps(result))
181
+ return
182
+
183
+ # Initialize or update state with document
184
+ if not state:
185
+ state = {
186
+ "version": "4.6.0",
187
+ "build_id": f"build-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
188
+ "status": "initializing"
189
+ }
190
+
191
+ state["project_spec"] = {
192
+ "source": "file",
193
+ "file_path": flags["from_document"],
194
+ "raw_content": content,
195
+ "format": fmt,
196
+ "loaded_at": datetime.now().isoformat(),
197
+ "word_count": len(content.split()),
198
+ "extracted": None, # Will be filled by Phase 0.5
199
+ "user_modifications": {
200
+ "added": [],
201
+ "removed": [],
202
+ "modified": []
203
+ }
204
+ }
205
+
206
+ save_hustle_build_state(state)
207
+
208
+ # Log the event
209
+ if UTILS_AVAILABLE:
210
+ log_workflow_event("project_document_loaded", {
211
+ "source": "file",
212
+ "file_path": flags["from_document"],
213
+ "format": fmt,
214
+ "word_count": len(content.split())
215
+ })
216
+
217
+ # Inject confirmation
218
+ result = {
219
+ "continue": True,
220
+ "additionalContext": f"""
221
+ ## Project Document Loaded
222
+
223
+ Successfully loaded project document:
224
+ - **Source:** `{flags["from_document"]}`
225
+ - **Format:** {fmt}
226
+ - **Size:** {len(content.split())} words
227
+
228
+ The document will be analyzed in Phase 0.5 to extract:
229
+ - Pages/routes
230
+ - Components
231
+ - APIs
232
+ - Data models
233
+ - External integrations
234
+
235
+ Proceeding to parse your build request...
236
+ """
237
+ }
238
+ print(json.dumps(result))
239
+ return
240
+
241
+ # No document provided - inject prompt asking for one
242
+ context = """
243
+ ## Project Document Intake
244
+
245
+ Before decomposing this build request, I need to check if you have a comprehensive project document.
246
+
247
+ **Do you have a project document (PRD, spec, deep research output)?**
248
+
249
+ A project document helps me:
250
+ - Identify ALL pages, components, and APIs upfront
251
+ - Build accurate dependency graphs
252
+ - Reference the spec throughout each sub-workflow
253
+ - Ensure nothing is missed
254
+
255
+ ### How to Provide a Document
256
+
257
+ **Option 1: File Path**
258
+ ```
259
+ I have a document at ./docs/my-prd.md
260
+ ```
261
+
262
+ **Option 2: Paste Content**
263
+ Just paste the document content directly in your next message.
264
+
265
+ **Option 3: URL**
266
+ ```
267
+ Fetch the document from https://example.com/my-spec.md
268
+ ```
269
+
270
+ **Option 4: No Document**
271
+ ```
272
+ No document, proceed with parsing my description
273
+ ```
274
+
275
+ ### Supported Formats
276
+ - Markdown (`.md`) - PRDs, specs, research outputs
277
+ - Plain text (`.txt`) - Notes, outlines
278
+ - JSON (`.json`) - Structured specs, API definitions
279
+
280
+ ---
281
+
282
+ _To skip this prompt in the future, use:_
283
+ ```
284
+ /hustle-build --skip-document [description]
285
+ ```
286
+
287
+ _Or provide a document directly:_
288
+ ```
289
+ /hustle-build --from-document ./docs/spec.md [description]
290
+ ```
291
+ """
292
+
293
+ result = {
294
+ "continue": True,
295
+ "additionalContext": context
296
+ }
297
+
298
+ print(json.dumps(result))
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Remote Question Proxy Hook
4
+
5
+ When REMOTE_QUESTIONS_ENABLED=true, this hook:
6
+ 1. Writes the current question to .claude/current-question.json
7
+ 2. Sends NTFY notification with link to the web UI
8
+ 3. Optionally waits for remote answer
9
+
10
+ Hook Type: PreToolUse (matcher: AskUserQuestion)
11
+ Version: 4.6.0
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import sys
17
+ import time
18
+ import subprocess
19
+ from pathlib import Path
20
+ from datetime import datetime
21
+
22
+ # Configuration
23
+ DEFAULT_PORT = 8765
24
+ POLL_INTERVAL = 2 # seconds
25
+ MAX_WAIT_TIME = 300 # 5 minutes
26
+
27
+
28
+ def get_project_dir():
29
+ """Get project directory from environment."""
30
+ return Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
31
+
32
+
33
+ def is_remote_questions_enabled():
34
+ """Check if remote questions feature is enabled."""
35
+ return os.environ.get("REMOTE_QUESTIONS_ENABLED", "").lower() == "true"
36
+
37
+
38
+ def get_remote_url():
39
+ """Get the remote URL (Cloudflare tunnel or localhost)."""
40
+ url = os.environ.get("REMOTE_QUESTIONS_URL", "")
41
+ if url:
42
+ return url.rstrip("/")
43
+
44
+ port = os.environ.get("REMOTE_QUESTIONS_PORT", DEFAULT_PORT)
45
+ return f"http://localhost:{port}"
46
+
47
+
48
+ def get_ntfy_topic():
49
+ """Get NTFY topic from environment."""
50
+ return os.environ.get("NTFY_TOPIC", "layers-mf-08ebf1d1")
51
+
52
+
53
+ def parse_question_input(tool_input_raw):
54
+ """Parse the AskUserQuestion tool input."""
55
+ try:
56
+ data = json.loads(tool_input_raw)
57
+ questions = data.get("questions", [])
58
+ return questions
59
+ except Exception:
60
+ return []
61
+
62
+
63
+ def write_question_file(questions, phase="unknown"):
64
+ """Write question to .claude/current-question.json for the server."""
65
+ project_dir = get_project_dir()
66
+ question_file = project_dir / ".claude" / "current-question.json"
67
+ question_file.parent.mkdir(parents=True, exist_ok=True)
68
+
69
+ # Format questions for the web UI
70
+ formatted_questions = []
71
+ for q in questions:
72
+ formatted_q = {
73
+ "id": q.get("header", "question").lower().replace(" ", "-"),
74
+ "question": q.get("question", ""),
75
+ "header": q.get("header", "Question"),
76
+ "options": [],
77
+ "multiSelect": q.get("multiSelect", False),
78
+ "timestamp": datetime.now().isoformat()
79
+ }
80
+
81
+ for opt in q.get("options", []):
82
+ formatted_q["options"].append({
83
+ "label": opt.get("label", ""),
84
+ "description": opt.get("description", "")
85
+ })
86
+
87
+ formatted_questions.append(formatted_q)
88
+
89
+ question_data = {
90
+ "questions": formatted_questions,
91
+ "phase": phase,
92
+ "created_at": datetime.now().isoformat(),
93
+ "status": "pending"
94
+ }
95
+
96
+ question_file.write_text(json.dumps(question_data, indent=2))
97
+ return question_file
98
+
99
+
100
+ def clear_answer_file():
101
+ """Clear any existing answer file."""
102
+ project_dir = get_project_dir()
103
+ answer_file = project_dir / ".claude" / "pending-answer.json"
104
+ if answer_file.exists():
105
+ answer_file.unlink()
106
+
107
+
108
+ def send_ntfy_notification(url):
109
+ """Send NTFY notification with link to question UI."""
110
+ topic = get_ntfy_topic()
111
+ message = f"[INPUT NEEDED] Answer question at: {url}"
112
+
113
+ try:
114
+ subprocess.run(
115
+ ["curl", "-s", "-d", message, f"ntfy.sh/{topic}"],
116
+ capture_output=True,
117
+ timeout=10
118
+ )
119
+ except Exception:
120
+ pass # Don't fail if notification fails
121
+
122
+
123
+ def wait_for_answer(timeout=MAX_WAIT_TIME):
124
+ """Wait for answer to appear in pending-answer.json."""
125
+ project_dir = get_project_dir()
126
+ answer_file = project_dir / ".claude" / "pending-answer.json"
127
+
128
+ start_time = time.time()
129
+
130
+ while time.time() - start_time < timeout:
131
+ if answer_file.exists():
132
+ try:
133
+ answer_data = json.loads(answer_file.read_text())
134
+ if answer_data.get("status") == "submitted":
135
+ return answer_data
136
+ except Exception:
137
+ pass
138
+
139
+ time.sleep(POLL_INTERVAL)
140
+
141
+ return None
142
+
143
+
144
+ def get_current_phase():
145
+ """Try to determine current workflow phase from state."""
146
+ project_dir = get_project_dir()
147
+
148
+ # Check hustle-build state
149
+ build_state_file = project_dir / ".claude" / "hustle-build-state.json"
150
+ if build_state_file.exists():
151
+ try:
152
+ state = json.loads(build_state_file.read_text())
153
+ phase = state.get("current_phase", "")
154
+ if phase:
155
+ return phase
156
+ except Exception:
157
+ pass
158
+
159
+ # Check api-dev state
160
+ api_state_file = project_dir / ".claude" / "api-dev-state.json"
161
+ if api_state_file.exists():
162
+ try:
163
+ state = json.loads(api_state_file.read_text())
164
+ phases = state.get("phases", {})
165
+ for phase_name, phase_data in phases.items():
166
+ if phase_data.get("status") == "in_progress":
167
+ return phase_name
168
+ except Exception:
169
+ pass
170
+
171
+ return "workflow"
172
+
173
+
174
+ def main():
175
+ # Check if remote questions is enabled
176
+ if not is_remote_questions_enabled():
177
+ # Not enabled, let the question proceed normally
178
+ print(json.dumps({"continue": True}))
179
+ return
180
+
181
+ # Read tool input
182
+ tool_input_raw = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
183
+
184
+ # Also check stdin for hook input
185
+ try:
186
+ if not sys.stdin.isatty():
187
+ stdin_data = sys.stdin.read()
188
+ if stdin_data:
189
+ try:
190
+ hook_input = json.loads(stdin_data)
191
+ tool_input_raw = json.dumps(hook_input.get("tool_input", {}))
192
+ except json.JSONDecodeError:
193
+ pass
194
+ except Exception:
195
+ pass
196
+
197
+ # Parse questions
198
+ questions = parse_question_input(tool_input_raw)
199
+
200
+ if not questions:
201
+ print(json.dumps({"continue": True}))
202
+ return
203
+
204
+ # Get current phase for context
205
+ phase = get_current_phase()
206
+
207
+ # Clear any existing answer
208
+ clear_answer_file()
209
+
210
+ # Write question to file for server
211
+ write_question_file(questions, phase)
212
+
213
+ # Get remote URL
214
+ remote_url = get_remote_url()
215
+
216
+ # Send NTFY notification
217
+ send_ntfy_notification(remote_url)
218
+
219
+ # Check if we should wait for remote answer
220
+ wait_mode = os.environ.get("REMOTE_QUESTIONS_WAIT", "false").lower() == "true"
221
+
222
+ if wait_mode:
223
+ # Wait for remote answer
224
+ answer = wait_for_answer()
225
+
226
+ if answer:
227
+ # Inject the answer as context
228
+ answers = answer.get("answers", {})
229
+
230
+ context = f"""
231
+ ## Remote Answer Received
232
+
233
+ The user answered remotely via the question interface:
234
+
235
+ ```json
236
+ {json.dumps(answers, indent=2)}
237
+ ```
238
+
239
+ Use these answers to proceed with the workflow.
240
+ """
241
+
242
+ # Clear the question file
243
+ project_dir = get_project_dir()
244
+ question_file = project_dir / ".claude" / "current-question.json"
245
+ if question_file.exists():
246
+ question_file.unlink()
247
+
248
+ print(json.dumps({
249
+ "continue": True,
250
+ "additionalContext": context
251
+ }))
252
+ return
253
+ else:
254
+ # Timeout - let the local question proceed
255
+ context = """
256
+ ## Remote Question Timeout
257
+
258
+ The remote question interface timed out waiting for an answer.
259
+ The question will be displayed locally instead.
260
+ """
261
+ print(json.dumps({
262
+ "continue": True,
263
+ "additionalContext": context
264
+ }))
265
+ return
266
+
267
+ # Non-blocking mode - just notify and continue with local question
268
+ context = f"""
269
+ ## Remote Question Notification Sent
270
+
271
+ A notification was sent to answer this question remotely at:
272
+ {remote_url}
273
+
274
+ The question will also be displayed here. Answer either locally or remotely.
275
+ """
276
+
277
+ print(json.dumps({
278
+ "continue": True,
279
+ "additionalContext": context
280
+ }))
281
+
282
+
283
+ if __name__ == "__main__":
284
+ main()