@josephyan/qingflow-cli 0.2.0-beta.67 → 0.2.0-beta.69

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
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.67
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.69
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.67 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.69 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.67",
3
+ "version": "0.2.0-beta.69",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b67"
7
+ version = "0.2.0b69"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -24,6 +24,8 @@ class PublicFieldType(str, Enum):
24
24
  address = "address"
25
25
  attachment = "attachment"
26
26
  boolean = "boolean"
27
+ q_linker = "q_linker"
28
+ code_block = "code_block"
27
29
  relation = "relation"
28
30
  subtable = "subtable"
29
31
 
@@ -46,6 +48,10 @@ FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
46
48
  "multi_select": PublicFieldType.multi_select,
47
49
  "multi-select": PublicFieldType.multi_select,
48
50
  "departments": PublicFieldType.department,
51
+ "qlinker": PublicFieldType.q_linker,
52
+ "q_linker": PublicFieldType.q_linker,
53
+ "codeblock": PublicFieldType.code_block,
54
+ "code_block": PublicFieldType.code_block,
49
55
  }
50
56
 
51
57
  FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
@@ -60,6 +66,8 @@ FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
60
66
  11: PublicFieldType.single_select,
61
67
  12: PublicFieldType.multi_select,
62
68
  13: PublicFieldType.attachment,
69
+ 20: PublicFieldType.q_linker,
70
+ 26: PublicFieldType.code_block,
63
71
  18: PublicFieldType.subtable,
64
72
  21: PublicFieldType.address,
65
73
  22: PublicFieldType.department,
@@ -281,6 +289,260 @@ class FieldSelector(StrictModel):
281
289
  return self
282
290
 
283
291
 
292
+ class CodeBlockAliasPathPatch(StrictModel):
293
+ alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
294
+ alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
295
+ alias_type: int = Field(default=1, validation_alias=AliasChoices("alias_type", "aliasType"))
296
+ alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
297
+ sub_alias: list["CodeBlockAliasPathPatch"] = Field(
298
+ default_factory=list,
299
+ validation_alias=AliasChoices("sub_alias", "subAlias"),
300
+ )
301
+
302
+
303
+ class CodeBlockConfigPatch(StrictModel):
304
+ config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
305
+ code_content: str = Field(default="", validation_alias=AliasChoices("code_content", "codeContent"))
306
+ being_hide_on_form: bool = Field(
307
+ default=False,
308
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
309
+ )
310
+ result_alias_path: list[CodeBlockAliasPathPatch] = Field(
311
+ default_factory=list,
312
+ validation_alias=AliasChoices("result_alias_path", "resultAliasPath", "alias_config", "aliasConfig"),
313
+ )
314
+
315
+
316
+ class CodeBlockInputBindingPatch(StrictModel):
317
+ field: FieldSelector
318
+ var: str | None = None
319
+
320
+ @model_validator(mode="before")
321
+ @classmethod
322
+ def normalize_aliases(cls, value: Any) -> Any:
323
+ if isinstance(value, str):
324
+ return {"field": {"name": value}}
325
+ if not isinstance(value, dict):
326
+ return value
327
+ payload = dict(value)
328
+ raw_field = payload.get("field")
329
+ if isinstance(raw_field, str):
330
+ payload["field"] = {"name": raw_field}
331
+ elif raw_field is None:
332
+ for key in ("field_name", "fieldName", "name", "title", "label"):
333
+ raw_value = payload.get(key)
334
+ if isinstance(raw_value, str) and raw_value.strip():
335
+ payload["field"] = {"name": raw_value}
336
+ break
337
+ return payload
338
+
339
+
340
+ class QLinkerInputSource(str, Enum):
341
+ query_param = "query_param"
342
+ header = "header"
343
+ url_encoded = "url_encoded"
344
+ json_path = "json_path"
345
+
346
+
347
+ class QLinkerKeyValuePatch(StrictModel):
348
+ key: str
349
+ value: str | None = None
350
+
351
+
352
+ class QLinkerAliasPathPatch(StrictModel):
353
+ alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
354
+ alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
355
+ alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
356
+
357
+
358
+ class RemoteLookupConfigPatch(StrictModel):
359
+ config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
360
+ url: str = ""
361
+ method: str = "GET"
362
+ headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
363
+ body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
364
+ url_encoded_value: list[QLinkerKeyValuePatch] = Field(
365
+ default_factory=list,
366
+ validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
367
+ )
368
+ json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
369
+ xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
370
+ result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
371
+ result_format_path: list[QLinkerAliasPathPatch] = Field(
372
+ default_factory=list,
373
+ validation_alias=AliasChoices("result_format_path", "resultFormatPath"),
374
+ )
375
+ query_params: list[QLinkerKeyValuePatch] = Field(
376
+ default_factory=list,
377
+ validation_alias=AliasChoices("query_params", "queryParams"),
378
+ )
379
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
380
+ custom_button_text_enabled: bool | None = Field(
381
+ default=None,
382
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
383
+ )
384
+ custom_button_text: str | None = Field(
385
+ default=None,
386
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
387
+ )
388
+ being_insert_value_directly: bool | None = Field(
389
+ default=None,
390
+ validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
391
+ )
392
+ being_hide_on_form: bool | None = Field(
393
+ default=None,
394
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
395
+ )
396
+
397
+
398
+ class QLinkerInputBindingPatch(StrictModel):
399
+ field: FieldSelector
400
+ key: str
401
+ source: QLinkerInputSource
402
+
403
+ @model_validator(mode="before")
404
+ @classmethod
405
+ def normalize_aliases(cls, value: Any) -> Any:
406
+ if isinstance(value, str):
407
+ return {"field": {"name": value}, "key": value, "source": QLinkerInputSource.query_param.value}
408
+ if not isinstance(value, dict):
409
+ return value
410
+ payload = dict(value)
411
+ raw_field = payload.get("field")
412
+ if isinstance(raw_field, str):
413
+ payload["field"] = {"name": raw_field}
414
+ elif raw_field is None:
415
+ for key in ("field_name", "fieldName", "name", "title", "label"):
416
+ raw_value = payload.get(key)
417
+ if isinstance(raw_value, str) and raw_value.strip():
418
+ payload["field"] = {"name": raw_value}
419
+ break
420
+ raw_source = payload.get("source")
421
+ if isinstance(raw_source, str):
422
+ payload["source"] = raw_source.strip().lower()
423
+ return payload
424
+
425
+
426
+ class QLinkerOutputBindingPatch(StrictModel):
427
+ alias: str
428
+ path: str
429
+ target_field: FieldSelector = Field(validation_alias=AliasChoices("target_field", "targetField"))
430
+
431
+ @model_validator(mode="before")
432
+ @classmethod
433
+ def normalize_aliases(cls, value: Any) -> Any:
434
+ if not isinstance(value, dict):
435
+ return value
436
+ payload = dict(value)
437
+ if "alias_name" in payload and "alias" not in payload:
438
+ payload["alias"] = payload.pop("alias_name")
439
+ if "aliasName" in payload and "alias" not in payload:
440
+ payload["alias"] = payload.pop("aliasName")
441
+ if "alias_path" in payload and "path" not in payload:
442
+ payload["path"] = payload.pop("alias_path")
443
+ if "aliasPath" in payload and "path" not in payload:
444
+ payload["path"] = payload.pop("aliasPath")
445
+ raw_target = payload.get("target_field", payload.get("targetField"))
446
+ if isinstance(raw_target, str):
447
+ payload["target_field"] = {"name": raw_target}
448
+ return payload
449
+
450
+
451
+ class QLinkerRequestPatch(StrictModel):
452
+ url: str = ""
453
+ method: str = "GET"
454
+ headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
455
+ query_params: list[QLinkerKeyValuePatch] = Field(
456
+ default_factory=list,
457
+ validation_alias=AliasChoices("query_params", "queryParams"),
458
+ )
459
+ body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
460
+ url_encoded_value: list[QLinkerKeyValuePatch] = Field(
461
+ default_factory=list,
462
+ validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
463
+ )
464
+ json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
465
+ xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
466
+ result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
467
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
468
+ custom_button_text_enabled: bool | None = Field(
469
+ default=None,
470
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
471
+ )
472
+ custom_button_text: str | None = Field(
473
+ default=None,
474
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
475
+ )
476
+ being_insert_value_directly: bool | None = Field(
477
+ default=None,
478
+ validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
479
+ )
480
+ being_hide_on_form: bool | None = Field(
481
+ default=None,
482
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
483
+ )
484
+
485
+
486
+ class QLinkerBindingPatch(StrictModel):
487
+ inputs: list[QLinkerInputBindingPatch] = Field(default_factory=list)
488
+ request: QLinkerRequestPatch
489
+ outputs: list[QLinkerOutputBindingPatch] = Field(default_factory=list)
490
+
491
+
492
+ class CodeBlockOutputBindingPatch(StrictModel):
493
+ alias: str
494
+ path: str
495
+ target_field: FieldSelector = Field(
496
+ validation_alias=AliasChoices("target_field", "targetField"),
497
+ )
498
+
499
+ @model_validator(mode="before")
500
+ @classmethod
501
+ def normalize_aliases(cls, value: Any) -> Any:
502
+ if not isinstance(value, dict):
503
+ return value
504
+ payload = dict(value)
505
+ if "alias_name" in payload and "alias" not in payload:
506
+ payload["alias"] = payload.pop("alias_name")
507
+ if "aliasName" in payload and "alias" not in payload:
508
+ payload["alias"] = payload.pop("aliasName")
509
+ if "alias_path" in payload and "path" not in payload:
510
+ payload["path"] = payload.pop("alias_path")
511
+ if "aliasPath" in payload and "path" not in payload:
512
+ payload["path"] = payload.pop("aliasPath")
513
+ raw_target = payload.get("target_field", payload.get("targetField"))
514
+ if isinstance(raw_target, str):
515
+ payload["target_field"] = {"name": raw_target}
516
+ return payload
517
+
518
+
519
+ class CodeBlockBindingPatch(StrictModel):
520
+ inputs: list[CodeBlockInputBindingPatch] = Field(default_factory=list)
521
+ code: str = ""
522
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
523
+ custom_button_text_enabled: bool | None = Field(
524
+ default=None,
525
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
526
+ )
527
+ custom_button_text: str | None = Field(
528
+ default=None,
529
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
530
+ )
531
+ outputs: list[CodeBlockOutputBindingPatch] = Field(default_factory=list)
532
+
533
+ @model_validator(mode="before")
534
+ @classmethod
535
+ def normalize_aliases(cls, value: Any) -> Any:
536
+ if not isinstance(value, dict):
537
+ return value
538
+ payload = dict(value)
539
+ if "code_content" in payload and "code" not in payload:
540
+ payload["code"] = payload.pop("code_content")
541
+ if "codeContent" in payload and "code" not in payload:
542
+ payload["code"] = payload.pop("codeContent")
543
+ return payload
544
+
545
+
284
546
  class FieldPatch(StrictModel):
285
547
  name: str = Field(validation_alias=AliasChoices("name", "title", "label"))
286
548
  type: PublicFieldType
@@ -291,6 +553,31 @@ class FieldPatch(StrictModel):
291
553
  display_field: FieldSelector | None = None
292
554
  visible_fields: list[FieldSelector] = Field(default_factory=list)
293
555
  relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
556
+ remote_lookup_config: RemoteLookupConfigPatch | None = Field(
557
+ default=None,
558
+ validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
559
+ )
560
+ q_linker_binding: QLinkerBindingPatch | None = Field(
561
+ default=None,
562
+ validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
563
+ )
564
+ code_block_config: CodeBlockConfigPatch | None = Field(
565
+ default=None,
566
+ validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
567
+ )
568
+ code_block_binding: CodeBlockBindingPatch | None = Field(
569
+ default=None,
570
+ validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
571
+ )
572
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
573
+ custom_button_text_enabled: bool | None = Field(
574
+ default=None,
575
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
576
+ )
577
+ custom_button_text: str | None = Field(
578
+ default=None,
579
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
580
+ )
294
581
  subfields: list["FieldPatch"] = Field(default_factory=list)
295
582
 
296
583
  @model_validator(mode="after")
@@ -301,6 +588,19 @@ class FieldPatch(StrictModel):
301
588
  raise ValueError("target_app_key is only allowed for relation fields")
302
589
  if self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
303
590
  raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
591
+ if self.type != PublicFieldType.q_linker and (
592
+ self.remote_lookup_config is not None
593
+ or self.q_linker_binding is not None
594
+ ):
595
+ raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
596
+ if self.type != PublicFieldType.code_block and (
597
+ self.code_block_config is not None
598
+ or self.code_block_binding is not None
599
+ or self.auto_trigger is not None
600
+ or self.custom_button_text_enabled is not None
601
+ or self.custom_button_text is not None
602
+ ):
603
+ raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
304
604
  if self.type == PublicFieldType.subtable and not self.subfields:
305
605
  raise ValueError("subtable field requires subfields")
306
606
  if self.type != PublicFieldType.subtable and self.subfields:
@@ -323,6 +623,31 @@ class FieldMutation(StrictModel):
323
623
  display_field: FieldSelector | None = None
324
624
  visible_fields: list[FieldSelector] | None = None
325
625
  relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
626
+ remote_lookup_config: RemoteLookupConfigPatch | None = Field(
627
+ default=None,
628
+ validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
629
+ )
630
+ q_linker_binding: QLinkerBindingPatch | None = Field(
631
+ default=None,
632
+ validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
633
+ )
634
+ code_block_config: CodeBlockConfigPatch | None = Field(
635
+ default=None,
636
+ validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
637
+ )
638
+ code_block_binding: CodeBlockBindingPatch | None = Field(
639
+ default=None,
640
+ validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
641
+ )
642
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
643
+ custom_button_text_enabled: bool | None = Field(
644
+ default=None,
645
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
646
+ )
647
+ custom_button_text: str | None = Field(
648
+ default=None,
649
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
650
+ )
326
651
  subfields: list[FieldPatch] | None = None
327
652
 
328
653
  @model_validator(mode="after")
@@ -331,6 +656,19 @@ class FieldMutation(StrictModel):
331
656
  raise ValueError("relation field requires target_app_key")
332
657
  if self.type is not None and self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
333
658
  raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
659
+ if self.type is not None and self.type != PublicFieldType.q_linker and (
660
+ self.remote_lookup_config is not None
661
+ or self.q_linker_binding is not None
662
+ ):
663
+ raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
664
+ if self.type is not None and self.type != PublicFieldType.code_block and (
665
+ self.code_block_config is not None
666
+ or self.code_block_binding is not None
667
+ or self.auto_trigger is not None
668
+ or self.custom_button_text_enabled is not None
669
+ or self.custom_button_text is not None
670
+ ):
671
+ raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
334
672
  if self.type == PublicFieldType.subtable and not self.subfields:
335
673
  raise ValueError("subtable field requires subfields")
336
674
  return self
@@ -385,8 +723,9 @@ def _normalize_layout_rows(value: Any, *, columns: int | None = None) -> Any:
385
723
 
386
724
 
387
725
  class LayoutSectionPatch(StrictModel):
388
- section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId"))
389
- title: str
726
+ type: str | None = Field(default=None, validation_alias=AliasChoices("type", "kind", "block_type", "blockType"))
727
+ section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId", "paragraph_id", "paragraphId"))
728
+ title: str = Field(validation_alias=AliasChoices("title", "name", "paragraph_title", "paragraphTitle"))
390
729
  rows: list[list[Any]] = Field(default_factory=list)
391
730
 
392
731
  @model_validator(mode="before")
@@ -397,6 +736,10 @@ class LayoutSectionPatch(StrictModel):
397
736
  payload = dict(value)
398
737
  if "name" in payload and "title" not in payload:
399
738
  payload["title"] = payload.pop("name")
739
+ if "paragraph_title" in payload and "title" not in payload:
740
+ payload["title"] = payload.pop("paragraph_title")
741
+ if "paragraphTitle" in payload and "title" not in payload:
742
+ payload["title"] = payload.pop("paragraphTitle")
400
743
  shorthand: Any | None = None
401
744
  if "rows" not in payload:
402
745
  if "fields" in payload:
@@ -412,6 +755,8 @@ class LayoutSectionPatch(StrictModel):
412
755
 
413
756
  @model_validator(mode="after")
414
757
  def validate_rows(self) -> "LayoutSectionPatch":
758
+ if self.type is not None and str(self.type).strip().lower() != "paragraph":
759
+ raise ValueError("layout section type must be 'paragraph'")
415
760
  if not self.rows:
416
761
  raise ValueError("section rows must be a non-empty list")
417
762
  for row in self.rows:
@@ -1211,5 +1556,6 @@ def _normalize_public_relation_mode(value: Any) -> str | None:
1211
1556
 
1212
1557
  CustomButtonMatchRulePatch.model_rebuild()
1213
1558
  CustomButtonAddDataConfigPatch.model_rebuild()
1559
+ CodeBlockAliasPathPatch.model_rebuild()
1214
1560
  ViewButtonBindingPatch.model_rebuild()
1215
1561
  ViewUpsertPatch.model_rebuild()