1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CommandSchema {
12 pub command: String,
13 #[serde(rename = "inputSchema")]
14 pub input_schema: serde_json::Value,
15 #[serde(rename = "outputSchema")]
16 pub output_schema: serde_json::Value,
17}
18
19impl CommandSchema {
20 fn wrap_in_envelope_schema(data_schema: serde_json::Value) -> serde_json::Value {
22 serde_json::json!({
23 "type": "object",
24 "required": ["ok", "type", "schemaVersion"],
25 "properties": {
26 "ok": { "type": "boolean" },
27 "type": { "type": "string" },
28 "schemaVersion": { "type": "integer", "const": 1 },
29 "data": data_schema,
30 "error": {
31 "type": "object",
32 "required": ["code", "message", "isRetryable"],
33 "properties": {
34 "code": { "type": "string" },
35 "message": { "type": "string" },
36 "isRetryable": { "type": "boolean" },
37 "details": { "type": "object" }
38 }
39 },
40 "meta": {
41 "type": "object",
42 "properties": {
43 "traceId": { "type": "string" }
44 }
45 }
46 }
47 })
48 }
49
50 fn timeline_output_schema() -> serde_json::Value {
52 serde_json::json!({
53 "type": "object",
54 "required": ["tweets"],
55 "properties": {
56 "tweets": {
57 "type": "array",
58 "items": {
59 "type": "object",
60 "required": ["id"],
61 "properties": {
62 "id": { "type": "string" },
63 "text": { "type": "string" },
64 "author_id": { "type": "string" },
65 "created_at": { "type": "string", "format": "date-time" }
66 }
67 }
68 },
69 "meta": {
70 "type": "object",
71 "properties": {
72 "pagination": {
73 "type": "object",
74 "properties": {
75 "next_token": { "type": "string" },
76 "previous_token": { "type": "string" }
77 }
78 }
79 }
80 }
81 }
82 })
83 }
84
85 pub fn for_command(command: &str) -> Self {
87 match command {
88 "commands" => Self {
89 command: command.to_string(),
90 input_schema: serde_json::json!({
91 "type": "object",
92 "properties": {},
93 "additionalProperties": false
94 }),
95 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
96 "type": "object",
97 "required": ["commands"],
98 "properties": {
99 "commands": {
100 "type": "array",
101 "items": {
102 "type": "object",
103 "required": ["name", "description", "arguments", "risk", "hasCost"],
104 "properties": {
105 "name": { "type": "string" },
106 "description": { "type": "string" },
107 "arguments": {
108 "type": "array",
109 "items": {
110 "type": "object",
111 "required": ["name", "description", "required", "type"],
112 "properties": {
113 "name": { "type": "string" },
114 "description": { "type": "string" },
115 "required": { "type": "boolean" },
116 "type": { "type": "string" },
117 "default": { "type": "string" }
118 }
119 }
120 },
121 "risk": {
122 "type": "string",
123 "enum": ["safe", "low", "medium", "high"]
124 },
125 "hasCost": { "type": "boolean" }
126 }
127 }
128 }
129 }
130 })),
131 },
132 "schema" => Self {
133 command: command.to_string(),
134 input_schema: serde_json::json!({
135 "type": "object",
136 "required": ["command"],
137 "properties": {
138 "command": { "type": "string" }
139 }
140 }),
141 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
142 "type": "object",
143 "required": ["command", "inputSchema", "outputSchema"],
144 "properties": {
145 "command": { "type": "string" },
146 "inputSchema": { "type": "object" },
147 "outputSchema": { "type": "object" }
148 }
149 })),
150 },
151 "help" => Self {
152 command: command.to_string(),
153 input_schema: serde_json::json!({
154 "type": "object",
155 "required": ["command"],
156 "properties": {
157 "command": { "type": "string" }
158 }
159 }),
160 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
161 "type": "object",
162 "required": ["command", "description", "usage", "exitCodes", "errorVocabulary", "examples"],
163 "properties": {
164 "command": { "type": "string" },
165 "description": { "type": "string" },
166 "usage": { "type": "string" },
167 "exitCodes": {
168 "type": "array",
169 "items": {
170 "type": "object",
171 "required": ["code", "description"],
172 "properties": {
173 "code": { "type": "integer" },
174 "description": { "type": "string" }
175 }
176 }
177 },
178 "errorVocabulary": {
179 "type": "array",
180 "items": {
181 "type": "object",
182 "required": ["code", "description", "isRetryable"],
183 "properties": {
184 "code": { "type": "string" },
185 "description": { "type": "string" },
186 "isRetryable": { "type": "boolean" }
187 }
188 }
189 },
190 "examples": {
191 "type": "array",
192 "items": {
193 "type": "object",
194 "required": ["description", "command"],
195 "properties": {
196 "description": { "type": "string" },
197 "command": { "type": "string" }
198 }
199 }
200 }
201 }
202 })),
203 },
204 "demo-interactive" => Self {
205 command: command.to_string(),
206 input_schema: serde_json::json!({
207 "type": "object",
208 "properties": {}
209 }),
210 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
211 "type": "object",
212 "required": ["message", "confirmed"],
213 "properties": {
214 "message": { "type": "string" },
215 "confirmed": { "type": "boolean" }
216 }
217 })),
218 },
219 "install-skills" => Self {
220 command: command.to_string(),
221 input_schema: serde_json::json!({
222 "type": "object",
223 "properties": {
224 "skill": { "type": "string" },
225 "agent": { "type": "string", "enum": ["claude", "opencode"] },
226 "global": { "type": "boolean", "default": false },
227 "yes": { "type": "boolean", "default": false }
228 },
229 "additionalProperties": false
230 }),
231 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
232 "type": "object",
233 "required": ["installed_skills"],
234 "properties": {
235 "installed_skills": {
236 "type": "array",
237 "items": {
238 "type": "object",
239 "required": ["name", "success", "canonical_path", "target_paths"],
240 "properties": {
241 "name": { "type": "string" },
242 "success": { "type": "boolean" },
243 "canonical_path": { "type": "string" },
244 "target_paths": {
245 "type": "array",
246 "items": { "type": "string" }
247 },
248 "error": { "type": "string" },
249 "used_symlink": { "type": "boolean" }
250 }
251 }
252 }
253 }
254 })),
255 },
256 "search recent" => Self {
257 command: command.to_string(),
258 input_schema: serde_json::json!({
259 "type": "object",
260 "required": ["query"],
261 "properties": {
262 "query": { "type": "string" },
263 "limit": { "type": "integer", "minimum": 1, "maximum": 100 },
264 "cursor": { "type": "string" }
265 },
266 "additionalProperties": false
267 }),
268 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
269 "type": "object",
270 "required": ["tweets"],
271 "properties": {
272 "tweets": {
273 "type": "array",
274 "items": {
275 "type": "object",
276 "required": ["id"],
277 "properties": {
278 "id": { "type": "string" },
279 "text": { "type": "string" },
280 "author_id": { "type": "string" },
281 "created_at": { "type": "string" }
282 }
283 }
284 },
285 "meta": {
286 "type": "object",
287 "properties": {
288 "pagination": {
289 "type": "object",
290 "required": ["result_count"],
291 "properties": {
292 "next_token": { "type": "string" },
293 "prev_token": { "type": "string" },
294 "result_count": { "type": "integer" }
295 }
296 }
297 }
298 }
299 }
300 })),
301 },
302 "search users" => Self {
303 command: command.to_string(),
304 input_schema: serde_json::json!({
305 "type": "object",
306 "required": ["query"],
307 "properties": {
308 "query": { "type": "string" },
309 "limit": { "type": "integer", "minimum": 1, "maximum": 100 },
310 "cursor": { "type": "string" }
311 },
312 "additionalProperties": false
313 }),
314 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
315 "type": "object",
316 "required": ["users"],
317 "properties": {
318 "users": {
319 "type": "array",
320 "items": {
321 "type": "object",
322 "required": ["id"],
323 "properties": {
324 "id": { "type": "string" },
325 "name": { "type": "string" },
326 "username": { "type": "string" },
327 "description": { "type": "string" }
328 }
329 }
330 },
331 "meta": {
332 "type": "object",
333 "properties": {
334 "pagination": {
335 "type": "object",
336 "required": ["result_count"],
337 "properties": {
338 "next_token": { "type": "string" },
339 "prev_token": { "type": "string" },
340 "result_count": { "type": "integer" }
341 }
342 }
343 }
344 }
345 }
346 })),
347 },
348 "tweets reply" => Self {
349 command: command.to_string(),
350 input_schema: serde_json::json!({
351 "type": "object",
352 "required": ["tweet_id", "text"],
353 "properties": {
354 "tweet_id": { "type": "string" },
355 "text": { "type": "string" },
356 "client_request_id": { "type": "string" },
357 "if_exists": {
358 "type": "string",
359 "enum": ["return", "error"],
360 "default": "return"
361 }
362 },
363 "additionalProperties": false
364 }),
365 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
366 "type": "object",
367 "required": ["tweet", "meta"],
368 "properties": {
369 "tweet": {
370 "type": "object",
371 "required": ["id"],
372 "properties": {
373 "id": { "type": "string" },
374 "text": { "type": "string" },
375 "author_id": { "type": "string" },
376 "created_at": { "type": "string" },
377 "conversation_id": { "type": "string" },
378 "referenced_tweets": { "type": "array" }
379 }
380 },
381 "meta": {
382 "type": "object",
383 "required": ["clientRequestId"],
384 "properties": {
385 "clientRequestId": { "type": "string" },
386 "fromCache": { "type": "boolean" }
387 }
388 }
389 }
390 })),
391 },
392 "tweets thread" => Self {
393 command: command.to_string(),
394 input_schema: serde_json::json!({
395 "type": "object",
396 "required": ["texts"],
397 "properties": {
398 "texts": {
399 "type": "array",
400 "items": { "type": "string" },
401 "minItems": 1
402 },
403 "client_request_id_prefix": { "type": "string" },
404 "if_exists": {
405 "type": "string",
406 "enum": ["return", "error"],
407 "default": "return"
408 }
409 },
410 "additionalProperties": false
411 }),
412 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
413 "type": "object",
414 "required": ["tweets", "meta"],
415 "properties": {
416 "tweets": {
417 "type": "array",
418 "items": {
419 "type": "object",
420 "required": ["id"],
421 "properties": {
422 "id": { "type": "string" },
423 "text": { "type": "string" }
424 }
425 }
426 },
427 "meta": {
428 "type": "object",
429 "required": ["count", "createdTweetIds"],
430 "properties": {
431 "count": { "type": "integer" },
432 "failedIndex": { "type": "integer" },
433 "createdTweetIds": {
434 "type": "array",
435 "items": { "type": "string" }
436 },
437 "fromCache": { "type": "boolean" }
438 }
439 }
440 }
441 })),
442 },
443 "tweets show" => Self {
444 command: command.to_string(),
445 input_schema: serde_json::json!({
446 "type": "object",
447 "required": ["tweet_id"],
448 "properties": {
449 "tweet_id": { "type": "string" }
450 },
451 "additionalProperties": false
452 }),
453 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
454 "type": "object",
455 "required": ["tweet"],
456 "properties": {
457 "tweet": {
458 "type": "object",
459 "required": ["id"],
460 "properties": {
461 "id": { "type": "string" },
462 "text": { "type": "string" },
463 "author_id": { "type": "string" },
464 "created_at": { "type": "string" },
465 "conversation_id": { "type": "string" },
466 "referenced_tweets": { "type": "array" }
467 }
468 }
469 }
470 })),
471 },
472 "tweets conversation" => Self {
473 command: command.to_string(),
474 input_schema: serde_json::json!({
475 "type": "object",
476 "required": ["tweet_id"],
477 "properties": {
478 "tweet_id": { "type": "string" }
479 },
480 "additionalProperties": false
481 }),
482 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
483 "type": "object",
484 "required": ["conversation_id", "posts", "edges"],
485 "properties": {
486 "conversation_id": {
487 "type": "string",
488 "description": "The conversation_id that identifies this conversation thread"
489 },
490 "posts": {
491 "type": "array",
492 "items": {
493 "type": "object",
494 "required": ["id"],
495 "properties": {
496 "id": { "type": "string" },
497 "text": { "type": "string" },
498 "conversation_id": { "type": "string" },
499 "referenced_tweets": { "type": "array" }
500 }
501 }
502 },
503 "edges": {
504 "type": "array",
505 "items": {
506 "type": "object",
507 "required": ["parent_id", "child_id"],
508 "properties": {
509 "parent_id": { "type": "string" },
510 "child_id": { "type": "string" }
511 }
512 }
513 }
514 }
515 })),
516 },
517 "timeline.home" | "timeline.mentions" => Self {
518 command: command.to_string(),
519 input_schema: serde_json::json!({
520 "type": "object",
521 "properties": {
522 "limit": {
523 "type": "integer",
524 "minimum": 1,
525 "maximum": 100,
526 "default": 10
527 },
528 "cursor": { "type": "string" }
529 },
530 "additionalProperties": false
531 }),
532 output_schema: Self::wrap_in_envelope_schema(Self::timeline_output_schema()),
533 },
534 "timeline.user" => Self {
535 command: command.to_string(),
536 input_schema: serde_json::json!({
537 "type": "object",
538 "required": ["handle"],
539 "properties": {
540 "handle": { "type": "string" },
541 "limit": {
542 "type": "integer",
543 "minimum": 1,
544 "maximum": 100,
545 "default": 10
546 },
547 "cursor": { "type": "string" }
548 },
549 "additionalProperties": false
550 }),
551 output_schema: Self::wrap_in_envelope_schema(Self::timeline_output_schema()),
552 },
553 "media.upload" => Self {
554 command: command.to_string(),
555 input_schema: serde_json::json!({
556 "type": "object",
557 "required": ["path"],
558 "properties": {
559 "path": {
560 "type": "string",
561 "description": "Path to the media file to upload"
562 }
563 },
564 "additionalProperties": false
565 }),
566 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
567 "type": "object",
568 "required": ["media_id"],
569 "properties": {
570 "media_id": {
571 "type": "string",
572 "description": "The media ID returned by the X API"
573 }
574 }
575 })),
576 },
577 "tweets like" | "tweets unlike" | "tweets retweet" | "tweets unretweet" => Self {
578 command: command.to_string(),
579 input_schema: serde_json::json!({
580 "type": "object",
581 "required": ["tweet_id"],
582 "properties": {
583 "tweet_id": { "type": "string", "description": "Tweet ID" }
584 },
585 "additionalProperties": false
586 }),
587 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
588 "type": "object",
589 "required": ["tweet_id", "success"],
590 "properties": {
591 "tweet_id": { "type": "string" },
592 "success": { "type": "boolean" }
593 }
594 })),
595 },
596 "bookmarks add" | "bookmarks remove" => Self {
597 command: command.to_string(),
598 input_schema: serde_json::json!({
599 "type": "object",
600 "required": ["tweet_id"],
601 "properties": {
602 "tweet_id": { "type": "string", "description": "Tweet ID" }
603 },
604 "additionalProperties": false
605 }),
606 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
607 "type": "object",
608 "required": ["tweet_id", "success"],
609 "properties": {
610 "tweet_id": { "type": "string" },
611 "success": { "type": "boolean" }
612 }
613 })),
614 },
615 "bookmarks list" => Self {
616 command: command.to_string(),
617 input_schema: serde_json::json!({
618 "type": "object",
619 "properties": {
620 "limit": { "type": "integer", "default": 10 },
621 "cursor": { "type": "string" }
622 },
623 "additionalProperties": false
624 }),
625 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
626 "type": "object",
627 "required": ["tweets"],
628 "properties": {
629 "tweets": {
630 "type": "array",
631 "items": {
632 "type": "object",
633 "required": ["id"],
634 "properties": {
635 "id": { "type": "string" },
636 "text": { "type": "string" },
637 "author_id": { "type": "string" },
638 "created_at": { "type": "string" }
639 }
640 }
641 },
642 "meta": {
643 "type": "object",
644 "properties": {
645 "pagination": {
646 "type": "object",
647 "properties": {
648 "next_token": { "type": "string" }
649 }
650 }
651 }
652 }
653 }
654 })),
655 },
656 _ => Self {
657 command: command.to_string(),
658 input_schema: serde_json::json!({
659 "type": "object",
660 "properties": {}
661 }),
662 output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
663 "type": "object",
664 "properties": {}
665 })),
666 },
667 }
668 }
669}