@josephyan/qingflow-cli 1.0.10 → 1.0.11

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@1.0.10
6
+ npm install @josephyan/qingflow-cli@1.0.11
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@1.0.10 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@1.0.11 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": "1.0.10",
3
+ "version": "1.0.11",
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 = "1.0.10"
7
+ version = "1.0.11"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -29,6 +29,7 @@ dependencies = [
29
29
  "openpyxl>=3.1,<4.0",
30
30
  "pydantic>=2.8,<3.0",
31
31
  "pycryptodome>=3.20,<4.0",
32
+ "pypdf>=5.0,<6.0",
32
33
  "python-socketio[client]>=5.11,<6.0",
33
34
  ]
34
35
 
@@ -56,7 +56,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
56
56
  list_parser.add_argument("--query-fields-file")
57
57
  list_parser.add_argument("--where-file")
58
58
  list_parser.add_argument("--order-by-file")
59
- list_parser.add_argument("--limit", type=int, default=20)
60
59
  list_parser.add_argument("--page", type=int, default=1)
61
60
  list_parser.add_argument("--view-id")
62
61
  list_parser.add_argument("--list-type", dest="legacy_list_type", type=int, help=argparse.SUPPRESS)
@@ -248,7 +247,6 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
248
247
  query_fields=_query_fields(args),
249
248
  where=load_list_arg(args.where_file, option_name="--where-file"),
250
249
  order_by=load_list_arg(args.order_by_file, option_name="--order-by-file"),
251
- limit=args.limit,
252
250
  page=args.page,
253
251
  view_id=args.view_id,
254
252
  )
@@ -190,20 +190,9 @@ def _format_record_list(result: dict[str, Any]) -> str:
190
190
  lines.append(f"- query: {lookup.get('query')}")
191
191
  lines.append(f"- confidence: {lookup.get('confidence')}")
192
192
  lines.append(f"- next_action: {lookup.get('next_action')}")
193
- lines.append(f"- reported_total: {lookup.get('reported_total')}")
194
- candidates = lookup.get("candidates") if isinstance(lookup.get("candidates"), list) else []
195
- for candidate in candidates[:5]:
196
- if not isinstance(candidate, dict):
197
- continue
198
- title = candidate.get("title") or "-"
199
- record_id = candidate.get("record_id") or "-"
200
- score = candidate.get("score")
201
- lines.append(f" - {record_id} | score={score} | {title}")
202
- matches = candidate.get("matched_fields") if isinstance(candidate.get("matched_fields"), list) else []
203
- if matches:
204
- match = matches[0]
205
- if isinstance(match, dict):
206
- lines.append(f" match: {match.get('title')}={match.get('value')}")
193
+ lines.append(f"- total_count: {lookup.get('total_count')}")
194
+ lines.append(f"- returned_count: {lookup.get('returned_count')}")
195
+ lines.append(f"- truncated: {lookup.get('truncated')}")
207
196
  lines.append(f"Returned Records: {len(items)}")
208
197
  for item in items[:10]:
209
198
  if isinstance(item, dict):
@@ -256,6 +245,15 @@ def _format_record_get(result: dict[str, Any]) -> str:
256
245
  media_items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
257
246
  downloaded_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
258
247
  failed_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
248
+ file_assets = result.get("file_assets") if isinstance(result.get("file_assets"), dict) else {}
249
+ file_items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
250
+ downloaded_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
251
+ failed_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
252
+ extracted_files = [
253
+ item
254
+ for item in downloaded_files
255
+ if isinstance(item.get("extraction"), dict) and item["extraction"].get("status") == "ok"
256
+ ]
259
257
  associated_resources = result.get("associated_resources") if isinstance(result.get("associated_resources"), list) else []
260
258
  unavailable_context = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
261
259
  lines = [
@@ -267,17 +265,26 @@ def _format_record_get(result: dict[str, Any]) -> str:
267
265
  f"Data logs: {data_logs.get('status') or '-'} / loaded={data_logs.get('items_loaded')}",
268
266
  f"Workflow logs: {workflow_logs.get('status') or '-'} / loaded={workflow_logs.get('items_loaded')}",
269
267
  f"Media assets: {media_assets.get('status') or '-'} / downloaded={len(downloaded_media)} / failed={len(failed_media)}",
268
+ f"File assets: {file_assets.get('status') or '-'} / downloaded={len(downloaded_files)} / extracted={len(extracted_files)} / failed={len(failed_files)}",
270
269
  f"Associated resources: {len(associated_resources)}",
271
270
  f"Unavailable contexts: {len(unavailable_context)}",
272
271
  ]
273
272
  if media_assets.get("local_dir"):
274
273
  lines.append(f"Media dir: {media_assets.get('local_dir')}")
274
+ if file_assets.get("local_dir"):
275
+ lines.append(f"File dir: {file_assets.get('local_dir')}")
275
276
  if failed_media:
276
277
  failure_counts: dict[str, int] = {}
277
278
  for item in failed_media:
278
279
  key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
279
280
  failure_counts[key] = failure_counts.get(key, 0) + 1
280
281
  lines.append("Media failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
282
+ if failed_files:
283
+ failure_counts = {}
284
+ for item in failed_files:
285
+ key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
286
+ failure_counts[key] = failure_counts.get(key, 0) + 1
287
+ lines.append("File failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
281
288
  summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
282
289
  if summary.get("text"):
283
290
  lines.append(f"Summary: {summary.get('text')}")
@@ -427,7 +427,7 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
427
427
  _trim_item_list(
428
428
  payload,
429
429
  "fields",
430
- allowed=("field_id", "title", "type", "value", "display_value", "requested_focus", "asset_ids"),
430
+ allowed=("field_id", "title", "type", "value", "display_value", "requested_focus", "asset_ids", "file_asset_ids"),
431
431
  )
432
432
  references = payload.get("references")
433
433
  if isinstance(references, list):
@@ -450,7 +450,7 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
450
450
  target_fields = item.get("target_fields") if isinstance(item.get("target_fields"), list) else []
451
451
  if target_fields:
452
452
  compact["target_fields"] = [
453
- _pick(field, ("field_id", "title", "type", "value", "display_value", "asset_ids"))
453
+ _pick(field, ("field_id", "title", "type", "value", "display_value", "asset_ids", "file_asset_ids"))
454
454
  for field in target_fields
455
455
  if isinstance(field, dict)
456
456
  ]
@@ -484,6 +484,37 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
484
484
  if isinstance(item, dict)
485
485
  ]
486
486
  payload["media_assets"] = compact_media
487
+ file_assets = payload.get("file_assets")
488
+ if isinstance(file_assets, dict):
489
+ compact_files = _pick(file_assets, ("status", "local_dir", "warnings"))
490
+ items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
491
+ compact_files["items"] = [
492
+ _pick(
493
+ item,
494
+ (
495
+ "file_asset_id",
496
+ "media_asset_id",
497
+ "kind",
498
+ "source",
499
+ "field_id",
500
+ "field_title",
501
+ "file_name",
502
+ "local_path",
503
+ "access_status",
504
+ "readable_by_agent",
505
+ "mime_type",
506
+ "size_bytes",
507
+ "download_strategy",
508
+ "storage_auth_type",
509
+ "storage_cookie_prefix",
510
+ "redirected",
511
+ "extraction",
512
+ ),
513
+ )
514
+ for item in items
515
+ if isinstance(item, dict)
516
+ ]
517
+ payload["file_assets"] = compact_files
487
518
  for key in ("data_logs", "workflow_logs"):
488
519
  node = payload.get(key)
489
520
  if not isinstance(node, dict):
@@ -519,17 +550,15 @@ def _trim_record_list(payload: JSONObject) -> None:
519
550
  if not isinstance(data, dict):
520
551
  return
521
552
  pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
522
- returned_items = pagination.get("returned_items")
523
- result_amount = pagination.get("result_amount")
524
- limit = pagination.get("limit")
525
- truncated = False
526
- if isinstance(result_amount, int) and isinstance(returned_items, int):
553
+ returned_items = pagination.get("returned_count", pagination.get("returned_items"))
554
+ result_amount = pagination.get("total_count", pagination.get("result_amount"))
555
+ truncated = bool(pagination.get("truncated"))
556
+ if not truncated and isinstance(result_amount, int) and isinstance(returned_items, int):
527
557
  truncated = result_amount > returned_items
528
558
  compact_pagination = {
529
559
  "loaded": True,
530
- "page_size": limit,
531
- "fetched_pages": 1,
532
- "reported_total": result_amount,
560
+ "returned_count": returned_items,
561
+ "total_count": result_amount,
533
562
  "truncated": truncated,
534
563
  }
535
564
  selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
@@ -544,33 +573,14 @@ def _trim_record_list(payload: JSONObject) -> None:
544
573
  payload["data"] = compact
545
574
  lookup = payload.get("lookup") if isinstance(payload.get("lookup"), dict) else {}
546
575
  if lookup:
547
- candidates = lookup.get("candidates") if isinstance(lookup.get("candidates"), list) else []
548
576
  payload["lookup"] = {
549
577
  "mode": lookup.get("mode"),
550
578
  "query": lookup.get("query"),
551
- "reported_total": lookup.get("reported_total"),
552
- "returned_candidates": lookup.get("returned_candidates"),
579
+ "total_count": lookup.get("total_count", lookup.get("reported_total")),
580
+ "returned_count": lookup.get("returned_count"),
581
+ "truncated": lookup.get("truncated"),
553
582
  "confidence": lookup.get("confidence"),
554
583
  "next_action": lookup.get("next_action"),
555
- "candidates": [
556
- {
557
- "record_id": candidate.get("record_id"),
558
- "title": candidate.get("title"),
559
- "score": candidate.get("score"),
560
- "matched_fields": [
561
- _pick(match, ("title", "value", "match_type", "score"))
562
- for match in (candidate.get("matched_fields") if isinstance(candidate.get("matched_fields"), list) else [])
563
- if isinstance(match, dict)
564
- ],
565
- "display_fields": [
566
- _pick(field, ("title", "value"))
567
- for field in (candidate.get("display_fields") if isinstance(candidate.get("display_fields"), list) else [])
568
- if isinstance(field, dict)
569
- ],
570
- }
571
- for candidate in candidates[:5]
572
- if isinstance(candidate, dict)
573
- ],
574
584
  }
575
585
 
576
586
 
@@ -69,7 +69,7 @@ Call `record_import_schema_get` when the import field mapping is unclear before
69
69
  `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
70
70
  `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
71
71
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
72
- `record_access.fields` and CSV columns use that exact same view schema.
72
+ `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
73
73
  `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
74
74
  `record_import_schema_get` returns import-ready column metadata.
75
75
 
@@ -124,8 +124,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
124
124
  - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
125
125
  - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
126
126
  - `record_delete` deletes by `record_id` or `record_ids`.
127
- - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets from rich text/attachments/image fields/subtables/reference targets, unavailable context, and `semantic_context`.
128
- - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
127
+ - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
128
+ - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
129
129
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
130
130
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
131
131
 
@@ -61,7 +61,7 @@ Call `record_import_schema_get` when the import field mapping is unclear before
61
61
  Inside `optional_fields`, any field with `may_become_required=true` is still writable, but may become required when linked visibility or option-driven runtime rules activate.
62
62
  `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
63
63
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
64
- `record_access.fields` and CSV columns use that exact same view schema; a missing field means it is not readable in that view.
64
+ `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema; a missing field means it is not readable in that view.
65
65
  `searchQueIds` is a backend full-text search scope, not an output-column/projection mechanism.
66
66
  `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
67
67
  `record_import_schema_get` returns import-ready column metadata.
@@ -125,8 +125,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
125
125
  - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
126
126
  - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
127
127
  - `record_delete` deletes by `record_id` or `record_ids`.
128
- - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets from rich text/attachments/image fields/subtables/reference targets, unavailable context, and `semantic_context`.
129
- - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
128
+ - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
129
+ - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
130
130
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
131
131
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
132
132