hap-cli 0.5.0__py3-none-any.whl

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 (58) hide show
  1. hap_cli/README.md +194 -0
  2. hap_cli/README_CN.md +601 -0
  3. hap_cli/__init__.py +3 -0
  4. hap_cli/commands/__init__.py +1 -0
  5. hap_cli/commands/ai_cmd.py +224 -0
  6. hap_cli/commands/app_cmd.py +308 -0
  7. hap_cli/commands/calendar_cmd.py +138 -0
  8. hap_cli/commands/chat_cmd.py +101 -0
  9. hap_cli/commands/config_cmd.py +169 -0
  10. hap_cli/commands/contact_cmd.py +125 -0
  11. hap_cli/commands/department_cmd.py +168 -0
  12. hap_cli/commands/group_cmd.py +128 -0
  13. hap_cli/commands/instance_cmd.py +310 -0
  14. hap_cli/commands/node_cmd.py +538 -0
  15. hap_cli/commands/optionset_cmd.py +99 -0
  16. hap_cli/commands/page_cmd.py +102 -0
  17. hap_cli/commands/plugin_cmd.py +133 -0
  18. hap_cli/commands/post_cmd.py +155 -0
  19. hap_cli/commands/record_cmd.py +228 -0
  20. hap_cli/commands/role_cmd.py +221 -0
  21. hap_cli/commands/workflow_cmd.py +284 -0
  22. hap_cli/commands/worksheet_cmd.py +342 -0
  23. hap_cli/context.py +43 -0
  24. hap_cli/core/__init__.py +1 -0
  25. hap_cli/core/ai.py +133 -0
  26. hap_cli/core/app.py +307 -0
  27. hap_cli/core/auth.py +219 -0
  28. hap_cli/core/calendar_mod.py +114 -0
  29. hap_cli/core/chat.py +73 -0
  30. hap_cli/core/contact.py +85 -0
  31. hap_cli/core/department.py +131 -0
  32. hap_cli/core/flow_node.py +1001 -0
  33. hap_cli/core/group.py +99 -0
  34. hap_cli/core/instance.py +572 -0
  35. hap_cli/core/optionset.py +112 -0
  36. hap_cli/core/page.py +138 -0
  37. hap_cli/core/plugin.py +87 -0
  38. hap_cli/core/post.py +118 -0
  39. hap_cli/core/record.py +268 -0
  40. hap_cli/core/role.py +227 -0
  41. hap_cli/core/session.py +348 -0
  42. hap_cli/core/workflow.py +556 -0
  43. hap_cli/core/worksheet.py +403 -0
  44. hap_cli/hap_cli.py +105 -0
  45. hap_cli/skills/SKILL.md +383 -0
  46. hap_cli/skills/__init__.py +0 -0
  47. hap_cli/tests/__init__.py +1 -0
  48. hap_cli/tests/test_core.py +1824 -0
  49. hap_cli/tests/test_full_e2e.py +136 -0
  50. hap_cli/tests/test_integration.py +805 -0
  51. hap_cli/utils/__init__.py +1 -0
  52. hap_cli/utils/formatting.py +111 -0
  53. hap_cli/utils/options.py +10 -0
  54. hap_cli-0.5.0.dist-info/METADATA +223 -0
  55. hap_cli-0.5.0.dist-info/RECORD +58 -0
  56. hap_cli-0.5.0.dist-info/WHEEL +5 -0
  57. hap_cli-0.5.0.dist-info/entry_points.txt +2 -0
  58. hap_cli-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,102 @@
1
+ """CLI commands for custom page management."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core import page as page_mod
7
+ from hap_cli.utils.formatting import output_json, output_table
8
+
9
+
10
+ @click.group()
11
+ def page():
12
+ """Custom page management."""
13
+ pass
14
+
15
+
16
+ @page.command("copy")
17
+ @click.argument("app_id")
18
+ @click.argument("page_id")
19
+ @click.option("--name", "-n", required=True, help="Name for the copied page")
20
+ @pass_context
21
+ def page_copy(ctx, app_id, page_id, name):
22
+ """Copy a custom page."""
23
+ try:
24
+ session = ctx.get_session()
25
+ data = page_mod.copy_page(session, app_id, page_id, name)
26
+ ctx.output(data, lambda d: click.echo(f"Page copied: {d}"))
27
+ except Exception as e:
28
+ ctx.handle_error(e)
29
+
30
+
31
+ @page.command("authorize")
32
+ @click.argument("app_id")
33
+ @click.argument("page_id")
34
+ @click.option("--role-id", "-r", required=True, help="Role ID to authorize")
35
+ @pass_context
36
+ def page_authorize(ctx, app_id, page_id, role_id):
37
+ """Add an authorization to a custom page."""
38
+ try:
39
+ session = ctx.get_session()
40
+ data = page_mod.add_authorize(session, app_id, page_id, role_id)
41
+ ctx.output(data, lambda d: click.echo(f"Authorization added: {d}"))
42
+ except Exception as e:
43
+ ctx.handle_error(e)
44
+
45
+
46
+ @page.command("permissions")
47
+ @click.argument("app_id")
48
+ @click.argument("page_id")
49
+ @pass_context
50
+ def page_permissions(ctx, app_id, page_id):
51
+ """Get authorizations for a custom page."""
52
+ try:
53
+ session = ctx.get_session()
54
+ data = page_mod.get_authorizes(session, app_id, page_id)
55
+ ctx.output(data, lambda d: output_json(d))
56
+ except Exception as e:
57
+ ctx.handle_error(e)
58
+
59
+
60
+ @page.command("edit-permission")
61
+ @click.argument("app_id")
62
+ @click.argument("page_id")
63
+ @click.argument("authorize_id")
64
+ @click.option("--status", "-s", required=True, type=int, help="Status value")
65
+ @pass_context
66
+ def page_edit_permission(ctx, app_id, page_id, authorize_id, status):
67
+ """Edit the status of a page authorization."""
68
+ try:
69
+ session = ctx.get_session()
70
+ data = page_mod.edit_authorize_status(session, app_id, page_id, authorize_id, status)
71
+ ctx.output(data, lambda d: click.echo("Permission updated."))
72
+ except Exception as e:
73
+ ctx.handle_error(e)
74
+
75
+
76
+ @page.command("delete-permission")
77
+ @click.argument("app_id")
78
+ @click.argument("page_id")
79
+ @click.argument("authorize_id")
80
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
81
+ @pass_context
82
+ def page_delete_permission(ctx, app_id, page_id, authorize_id, yes):
83
+ """Delete a page authorization."""
84
+ try:
85
+ if not yes:
86
+ click.confirm(f"Delete authorization {authorize_id}?", abort=True)
87
+ session = ctx.get_session()
88
+ data = page_mod.delete_authorize(session, app_id, page_id, authorize_id)
89
+ ctx.output(data, lambda d: click.echo("Permission deleted."))
90
+ except Exception as e:
91
+ ctx.handle_error(e)
92
+
93
+
94
+ @page.command("component-types")
95
+ @pass_context
96
+ def page_component_types(ctx):
97
+ """Show available custom page component types."""
98
+ rows = [{"type": k, "description": v} for k, v in page_mod.PAGE_COMPONENT_TYPES.items()]
99
+ ctx.output(
100
+ rows,
101
+ lambda d: output_table(d, ["type", "description"], ["Component Type", "Description"]),
102
+ )
@@ -0,0 +1,133 @@
1
+ """CLI commands for plugin management."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core import plugin as plugin_mod
7
+ from hap_cli.utils.formatting import output_json, output_kv
8
+
9
+
10
+ @click.group()
11
+ def plugin():
12
+ """Plugin management."""
13
+ pass
14
+
15
+
16
+ @plugin.command("list")
17
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
18
+ @click.option("--page-size", "-n", default=20, help="Items per page")
19
+ @click.option("--page", default=1, help="Page number")
20
+ @pass_context
21
+ def plugin_list(ctx, project_id, page_size, page):
22
+ """List plugins."""
23
+ try:
24
+ session = ctx.get_session()
25
+ pid = project_id or session.default_project_id
26
+ if not pid:
27
+ raise click.UsageError("Project ID required.")
28
+ data = plugin_mod.get_plugins(session, pid, page_index=page, page_size=page_size)
29
+ ctx.output(data, lambda d: output_json(d))
30
+ except Exception as e:
31
+ ctx.handle_error(e)
32
+
33
+
34
+ @plugin.command("get")
35
+ @click.argument("plugin_id")
36
+ @pass_context
37
+ def plugin_get(ctx, plugin_id):
38
+ """Get plugin detail."""
39
+ try:
40
+ session = ctx.get_session()
41
+ data = plugin_mod.get_plugin_detail(session, plugin_id)
42
+ ctx.output(data, lambda d: output_kv(d))
43
+ except Exception as e:
44
+ ctx.handle_error(e)
45
+
46
+
47
+ @plugin.command("create")
48
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
49
+ @click.option("--name", "-n", required=True, help="Plugin name")
50
+ @click.option("--desc", "-d", default="", help="Description")
51
+ @pass_context
52
+ def plugin_create(ctx, project_id, name, desc):
53
+ """Create a new plugin."""
54
+ try:
55
+ session = ctx.get_session()
56
+ pid = project_id or session.default_project_id
57
+ if not pid:
58
+ raise click.UsageError("Project ID required.")
59
+ data = plugin_mod.create_plugin(session, pid, name, description=desc)
60
+ ctx.output(data, lambda d: click.echo(f"Plugin created: {d}"))
61
+ except Exception as e:
62
+ ctx.handle_error(e)
63
+
64
+
65
+ @plugin.command("edit")
66
+ @click.argument("plugin_id")
67
+ @click.option("--name", "-n", default="", help="New name")
68
+ @click.option("--desc", "-d", default="", help="New description")
69
+ @pass_context
70
+ def plugin_edit(ctx, plugin_id, name, desc):
71
+ """Edit plugin name and/or description."""
72
+ try:
73
+ session = ctx.get_session()
74
+ data = plugin_mod.edit_plugin(session, plugin_id, name=name, description=desc)
75
+ ctx.output(data, lambda d: click.echo("Plugin updated."))
76
+ except Exception as e:
77
+ ctx.handle_error(e)
78
+
79
+
80
+ @plugin.command("delete")
81
+ @click.argument("plugin_id")
82
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
83
+ @pass_context
84
+ def plugin_delete(ctx, plugin_id, yes):
85
+ """Delete a plugin."""
86
+ try:
87
+ if not yes:
88
+ click.confirm(f"Delete plugin {plugin_id}?", abort=True)
89
+ session = ctx.get_session()
90
+ data = plugin_mod.remove_plugin(session, plugin_id)
91
+ ctx.output(data, lambda d: click.echo("Plugin deleted."))
92
+ except Exception as e:
93
+ ctx.handle_error(e)
94
+
95
+
96
+ @plugin.command("release")
97
+ @click.argument("plugin_id")
98
+ @pass_context
99
+ def plugin_release(ctx, plugin_id):
100
+ """Release a new version of a plugin."""
101
+ try:
102
+ session = ctx.get_session()
103
+ data = plugin_mod.release_plugin(session, plugin_id)
104
+ ctx.output(data, lambda d: click.echo(f"Plugin released: {d}"))
105
+ except Exception as e:
106
+ ctx.handle_error(e)
107
+
108
+
109
+ @plugin.command("rollback")
110
+ @click.argument("plugin_id")
111
+ @click.argument("version_id")
112
+ @pass_context
113
+ def plugin_rollback(ctx, plugin_id, version_id):
114
+ """Rollback a plugin to a specific version."""
115
+ try:
116
+ session = ctx.get_session()
117
+ data = plugin_mod.rollback_plugin(session, plugin_id, version_id)
118
+ ctx.output(data, lambda d: click.echo(f"Plugin rolled back to {version_id}."))
119
+ except Exception as e:
120
+ ctx.handle_error(e)
121
+
122
+
123
+ @plugin.command("history")
124
+ @click.argument("plugin_id")
125
+ @pass_context
126
+ def plugin_history(ctx, plugin_id):
127
+ """Get plugin release history."""
128
+ try:
129
+ session = ctx.get_session()
130
+ data = plugin_mod.get_release_history(session, plugin_id)
131
+ ctx.output(data, lambda d: output_json(d))
132
+ except Exception as e:
133
+ ctx.handle_error(e)
@@ -0,0 +1,155 @@
1
+ """CLI commands for feed/post management."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core import post as post_mod
7
+ from hap_cli.utils.formatting import output_json, output_kv
8
+
9
+
10
+ @click.group()
11
+ def post():
12
+ """Feed and post management."""
13
+ pass
14
+
15
+
16
+ @post.command("list")
17
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
18
+ @click.option("--type", "post_type", default=0, type=int, help="Post type")
19
+ @click.option("--page-size", "-n", default=20, help="Items per page")
20
+ @click.option("--page", default=1, help="Page number")
21
+ @pass_context
22
+ def post_list(ctx, project_id, post_type, page_size, page):
23
+ """List feed posts."""
24
+ try:
25
+ session = ctx.get_session()
26
+ pid = project_id or session.default_project_id
27
+ if not pid:
28
+ raise click.UsageError("Project ID required.")
29
+ data = post_mod.get_post_list(session, pid, post_type=post_type, page_index=page, page_size=page_size)
30
+ ctx.output(data, lambda d: output_json(d))
31
+ except Exception as e:
32
+ ctx.handle_error(e)
33
+
34
+
35
+ @post.command("get")
36
+ @click.argument("post_id")
37
+ @pass_context
38
+ def post_get(ctx, post_id):
39
+ """Get post detail."""
40
+ try:
41
+ session = ctx.get_session()
42
+ data = post_mod.get_post_detail(session, post_id)
43
+ ctx.output(data, lambda d: output_kv(d))
44
+ except Exception as e:
45
+ ctx.handle_error(e)
46
+
47
+
48
+ @post.command("create")
49
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
50
+ @click.option("--type", "post_type", default=0, type=int, help="Post type")
51
+ @click.option("--message", "-m", required=True, help="Post content")
52
+ @click.option("--scope", default=0, type=int, help="Visibility scope")
53
+ @pass_context
54
+ def post_create(ctx, project_id, post_type, message, scope):
55
+ """Create a new post."""
56
+ try:
57
+ session = ctx.get_session()
58
+ pid = project_id or session.default_project_id
59
+ if not pid:
60
+ raise click.UsageError("Project ID required.")
61
+ data = post_mod.add_post(session, pid, post_type, message, scope=scope)
62
+ ctx.output(data, lambda d: click.echo(f"Post created: {d}"))
63
+ except Exception as e:
64
+ ctx.handle_error(e)
65
+
66
+
67
+ @post.command("update")
68
+ @click.argument("post_id")
69
+ @click.option("--message", "-m", required=True, help="New content")
70
+ @pass_context
71
+ def post_update(ctx, post_id, message):
72
+ """Update a post."""
73
+ try:
74
+ session = ctx.get_session()
75
+ data = post_mod.edit_post(session, post_id, message)
76
+ ctx.output(data, lambda d: click.echo("Post updated."))
77
+ except Exception as e:
78
+ ctx.handle_error(e)
79
+
80
+
81
+ @post.command("delete")
82
+ @click.argument("post_id")
83
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
84
+ @pass_context
85
+ def post_delete(ctx, post_id, yes):
86
+ """Delete a post."""
87
+ try:
88
+ if not yes:
89
+ click.confirm(f"Delete post {post_id}?", abort=True)
90
+ session = ctx.get_session()
91
+ data = post_mod.remove_post(session, post_id)
92
+ ctx.output(data, lambda d: click.echo("Post deleted."))
93
+ except Exception as e:
94
+ ctx.handle_error(e)
95
+
96
+
97
+ @post.command("comment")
98
+ @click.argument("post_id")
99
+ @click.option("--message", "-m", required=True, help="Comment content")
100
+ @click.option("--reply-id", default="", help="Reply to comment ID")
101
+ @pass_context
102
+ def post_comment(ctx, post_id, message, reply_id):
103
+ """Add a comment to a post."""
104
+ try:
105
+ session = ctx.get_session()
106
+ data = post_mod.add_comment(session, post_id, message, reply_id=reply_id)
107
+ ctx.output(data, lambda d: click.echo("Comment added."))
108
+ except Exception as e:
109
+ ctx.handle_error(e)
110
+
111
+
112
+ @post.command("comments")
113
+ @click.argument("post_id")
114
+ @click.option("--page-size", "-n", default=20, help="Items per page")
115
+ @click.option("--page", default=1, help="Page number")
116
+ @pass_context
117
+ def post_comments(ctx, post_id, page_size, page):
118
+ """List comments for a post."""
119
+ try:
120
+ session = ctx.get_session()
121
+ data = post_mod.get_comments(session, post_id, page_index=page, page_size=page_size)
122
+ ctx.output(data, lambda d: output_json(d))
123
+ except Exception as e:
124
+ ctx.handle_error(e)
125
+
126
+
127
+ @post.command("like")
128
+ @click.argument("post_id")
129
+ @pass_context
130
+ def post_like(ctx, post_id):
131
+ """Like or unlike a post."""
132
+ try:
133
+ session = ctx.get_session()
134
+ data = post_mod.like_post(session, post_id)
135
+ ctx.output(data, lambda d: click.echo("Like toggled."))
136
+ except Exception as e:
137
+ ctx.handle_error(e)
138
+
139
+
140
+ @post.command("top")
141
+ @click.argument("post_id")
142
+ @click.option("--remove", is_flag=True, help="Remove from top")
143
+ @pass_context
144
+ def post_top(ctx, post_id, remove):
145
+ """Pin or unpin a post to top."""
146
+ try:
147
+ session = ctx.get_session()
148
+ if remove:
149
+ data = post_mod.remove_top_post(session, post_id)
150
+ ctx.output(data, lambda d: click.echo("Post unpinned."))
151
+ else:
152
+ data = post_mod.add_top_post(session, post_id)
153
+ ctx.output(data, lambda d: click.echo("Post pinned."))
154
+ except Exception as e:
155
+ ctx.handle_error(e)
@@ -0,0 +1,228 @@
1
+ """CLI commands for record CRUD operations."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core import record as rec_mod
7
+ from hap_cli.utils.formatting import output_json, output_kv, format_record_row
8
+
9
+
10
+ @click.group()
11
+ def record():
12
+ """Record CRUD operations."""
13
+ pass
14
+
15
+
16
+ @record.command("list")
17
+ @click.argument("worksheet_id")
18
+ @click.option("--view-id", "-v", default="", help="View ID")
19
+ @click.option("--page-size", "-n", default=20, help="Records per page")
20
+ @click.option("--page", "-p", default=1, help="Page number")
21
+ @click.option("--keywords", "-k", default="", help="Search keywords")
22
+ @click.option("--sort", "-s", default="", help="Sort by field ID")
23
+ @click.option("--asc", is_flag=True, help="Sort ascending")
24
+ @pass_context
25
+ def record_list(ctx, worksheet_id, view_id, page_size, page, keywords, sort, asc):
26
+ """Query records with filtering and pagination."""
27
+ try:
28
+ session = ctx.get_session()
29
+ result = rec_mod.get_records(
30
+ session, worksheet_id, view_id,
31
+ page_size=page_size, page_index=page,
32
+ keywords=keywords, sort_id=sort, is_asc=asc,
33
+ )
34
+ ctx.output(
35
+ result,
36
+ lambda d: _print_records(d),
37
+ )
38
+ except Exception as e:
39
+ ctx.handle_error(e)
40
+
41
+
42
+ def _print_records(result):
43
+ """Print records in human-readable format."""
44
+ records = result.get("data", [])
45
+ count = result.get("count", 0)
46
+ click.echo(f"Total: {count} records\n")
47
+ if not records:
48
+ click.echo("(no records)")
49
+ return
50
+ for i, rec in enumerate(records):
51
+ formatted = format_record_row(rec)
52
+ rid = formatted.pop("rowId", "?")
53
+ click.echo(f"── Record {i + 1} [{rid}] ──")
54
+ for k, v in formatted.items():
55
+ if v:
56
+ click.echo(f" {k}: {v}")
57
+ click.echo()
58
+
59
+
60
+ @record.command("get")
61
+ @click.argument("worksheet_id")
62
+ @click.argument("row_id")
63
+ @click.option("--view-id", "-v", default="", help="View ID")
64
+ @pass_context
65
+ def record_get(ctx, worksheet_id, row_id, view_id):
66
+ """Get a single record by ID."""
67
+ try:
68
+ session = ctx.get_session()
69
+ data = rec_mod.get_record(session, worksheet_id, row_id, view_id)
70
+ ctx.output(data, lambda d: output_kv(format_record_row(d)))
71
+ except Exception as e:
72
+ ctx.handle_error(e)
73
+
74
+
75
+ @record.command("create")
76
+ @click.argument("worksheet_id")
77
+ @click.option("--field", "-f", multiple=True, help="Field value: CONTROL_ID=VALUE")
78
+ @click.option("--no-workflow", is_flag=True, help="Don't trigger workflows")
79
+ @pass_context
80
+ def record_create(ctx, worksheet_id, field, no_workflow):
81
+ """Create a new record. Use -f CONTROL_ID=VALUE for each field."""
82
+ try:
83
+ session = ctx.get_session()
84
+ controls = _parse_fields(field)
85
+ data = rec_mod.create_record(
86
+ session, worksheet_id, controls,
87
+ trigger_workflow=not no_workflow,
88
+ )
89
+ ctx.output(data, lambda d: click.echo(f"Created: {d}"))
90
+ except Exception as e:
91
+ ctx.handle_error(e)
92
+
93
+
94
+ @record.command("update")
95
+ @click.argument("worksheet_id")
96
+ @click.argument("row_id")
97
+ @click.option("--field", "-f", multiple=True, help="Field value: CONTROL_ID=VALUE")
98
+ @click.option("--no-workflow", is_flag=True, help="Don't trigger workflows")
99
+ @pass_context
100
+ def record_update(ctx, worksheet_id, row_id, field, no_workflow):
101
+ """Update a record. Use -f CONTROL_ID=VALUE for each field."""
102
+ try:
103
+ session = ctx.get_session()
104
+ controls = _parse_fields(field)
105
+ data = rec_mod.update_record(
106
+ session, worksheet_id, row_id, controls,
107
+ trigger_workflow=not no_workflow,
108
+ )
109
+ ctx.output(data, lambda d: click.echo(f"Updated: {d}"))
110
+ except Exception as e:
111
+ ctx.handle_error(e)
112
+
113
+
114
+ @record.command("delete")
115
+ @click.argument("worksheet_id")
116
+ @click.argument("row_ids", nargs=-1, required=True)
117
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
118
+ @pass_context
119
+ def record_delete(ctx, worksheet_id, row_ids, yes):
120
+ """Delete records by IDs."""
121
+ try:
122
+ if not yes:
123
+ click.confirm(f"Delete {len(row_ids)} record(s)?", abort=True)
124
+ session = ctx.get_session()
125
+ data = rec_mod.delete_records(session, worksheet_id, list(row_ids))
126
+ ctx.output(data, lambda d: click.echo(f"Deleted {len(row_ids)} record(s)."))
127
+ except Exception as e:
128
+ ctx.handle_error(e)
129
+
130
+
131
+ @record.command("discussions")
132
+ @click.argument("worksheet_id")
133
+ @click.argument("row_id")
134
+ @click.option("--page-size", "-n", default=20, help="Discussions per page")
135
+ @click.option("--page", "-p", default=1, help="Page number")
136
+ @pass_context
137
+ def record_discussions(ctx, worksheet_id, row_id, page_size, page):
138
+ """Get discussions for a record."""
139
+ try:
140
+ session = ctx.get_session()
141
+ data = rec_mod.get_discussions(
142
+ session, row_id, source_type=7,
143
+ page_index=page, page_size=page_size,
144
+ )
145
+ ctx.output(data, lambda d: output_json(d))
146
+ except Exception as e:
147
+ ctx.handle_error(e)
148
+
149
+
150
+ @record.command("add-discussion")
151
+ @click.argument("worksheet_id")
152
+ @click.argument("row_id")
153
+ @click.option("--message", "-m", required=True, help="Discussion message")
154
+ @click.option("--app-id", default="", help="Application ID")
155
+ @pass_context
156
+ def record_add_discussion(ctx, worksheet_id, row_id, message, app_id):
157
+ """Add a discussion/comment to a record."""
158
+ try:
159
+ session = ctx.get_session()
160
+ data = rec_mod.add_discussion(
161
+ session, row_id, message, source_type=7, app_id=app_id,
162
+ )
163
+ ctx.output(data, lambda d: click.echo(f"Discussion added."))
164
+ except Exception as e:
165
+ ctx.handle_error(e)
166
+
167
+
168
+ @record.command("delete-discussion")
169
+ @click.argument("discussion_id")
170
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
171
+ @pass_context
172
+ def record_delete_discussion(ctx, discussion_id, yes):
173
+ """Delete a discussion/comment."""
174
+ try:
175
+ if not yes:
176
+ click.confirm(f"Delete discussion {discussion_id}?", abort=True)
177
+ session = ctx.get_session()
178
+ data = rec_mod.remove_discussion(session, discussion_id, source_type=7)
179
+ ctx.output(data, lambda d: click.echo(f"Discussion deleted."))
180
+ except Exception as e:
181
+ ctx.handle_error(e)
182
+
183
+
184
+ @record.command("pivot")
185
+ @click.argument("worksheet_id")
186
+ @click.option("--view-id", "-v", default="", help="View ID")
187
+ @click.option("--app-id", default="", help="Application ID")
188
+ @pass_context
189
+ def record_pivot(ctx, worksheet_id, view_id, app_id):
190
+ """Get pivot table / report data for a worksheet."""
191
+ try:
192
+ session = ctx.get_session()
193
+ data = rec_mod.get_pivot_data(
194
+ session, worksheet_id, view_id=view_id, app_id=app_id,
195
+ )
196
+ ctx.output(data, lambda d: output_json(d))
197
+ except Exception as e:
198
+ ctx.handle_error(e)
199
+
200
+
201
+ @record.command("logs")
202
+ @click.argument("worksheet_id")
203
+ @click.argument("row_id")
204
+ @click.option("--page-size", "-n", default=50, help="Logs per page")
205
+ @click.option("--page", "-p", default=1, help="Page number")
206
+ @pass_context
207
+ def record_logs(ctx, worksheet_id, row_id, page_size, page):
208
+ """Get operation logs for a record."""
209
+ try:
210
+ session = ctx.get_session()
211
+ data = rec_mod.get_record_logs(
212
+ session, worksheet_id, row_id,
213
+ page_index=page, page_size=page_size,
214
+ )
215
+ ctx.output(data, lambda d: output_json(d))
216
+ except Exception as e:
217
+ ctx.handle_error(e)
218
+
219
+
220
+ def _parse_fields(fields: tuple) -> list[dict]:
221
+ """Parse -f CONTROL_ID=VALUE pairs into controls list."""
222
+ controls = []
223
+ for f in fields:
224
+ if "=" not in f:
225
+ raise click.UsageError(f"Invalid field format: {f}. Use CONTROL_ID=VALUE")
226
+ cid, value = f.split("=", 1)
227
+ controls.append({"controlId": cid.strip(), "value": value.strip()})
228
+ return controls