better-notion 1.0.0__py3-none-any.whl → 1.1.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.
@@ -0,0 +1,1767 @@
1
+ """CLI commands for workflow entities.
2
+
3
+ This module provides CRUD commands for all workflow entities including
4
+ Organizations, Projects, Versions, Tasks, Ideas, Work Issues, and Incidents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from typing import Optional
11
+
12
+ import typer
13
+
14
+ from better_notion._cli.response import format_error, format_success
15
+ from better_notion._sdk.client import NotionClient
16
+ from better_notion.utils.agents import ProjectContext, get_or_create_agent_id
17
+
18
+
19
+ def get_client() -> NotionClient:
20
+ """Get authenticated Notion client."""
21
+ from better_notion._cli.config import Config
22
+
23
+ config = Config.load()
24
+ return NotionClient(auth=config.token, timeout=config.timeout)
25
+
26
+
27
+ def get_workspace_config() -> dict:
28
+ """Get workspace configuration."""
29
+ import json
30
+ from pathlib import Path
31
+
32
+ config_path = Path.home() / ".notion" / "workspace.json"
33
+ if not config_path.exists():
34
+ return {}
35
+
36
+ with open(config_path) as f:
37
+ return json.load(f)
38
+
39
+
40
+ # ===== ORGANIZATIONS =====
41
+
42
+ def orgs_list() -> str:
43
+ """
44
+ List all organizations.
45
+
46
+ Example:
47
+ $ notion orgs list
48
+ """
49
+ async def _list() -> str:
50
+ try:
51
+ client = get_client()
52
+
53
+ # Register SDK plugin
54
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
55
+ plugin = AgentsSDKPlugin()
56
+ plugin.initialize(client)
57
+ client.register_sdk_plugin(plugin)
58
+
59
+ # Get manager
60
+ manager = client.plugin_manager("organizations")
61
+ orgs = await manager.list()
62
+
63
+ return format_success({
64
+ "organizations": [
65
+ {
66
+ "id": org.id,
67
+ "name": org.name,
68
+ "slug": org.slug,
69
+ "status": org.status,
70
+ }
71
+ for org in orgs
72
+ ],
73
+ "total": len(orgs),
74
+ })
75
+
76
+ except Exception as e:
77
+ return format_error("LIST_ORGS_ERROR", str(e), retry=False)
78
+
79
+ return asyncio.run(_list())
80
+
81
+
82
+ def orgs_get(org_id: str) -> str:
83
+ """
84
+ Get an organization by ID.
85
+
86
+ Args:
87
+ org_id: Organization page ID
88
+
89
+ Example:
90
+ $ notion orgs get org_123
91
+ """
92
+ async def _get() -> str:
93
+ try:
94
+ client = get_client()
95
+
96
+ # Register SDK plugin
97
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
98
+ plugin = AgentsSDKPlugin()
99
+ plugin.initialize(client)
100
+ client.register_sdk_plugin(plugin)
101
+
102
+ # Get organization
103
+ manager = client.plugin_manager("organizations")
104
+ org = await manager.get(org_id)
105
+
106
+ return format_success({
107
+ "id": org.id,
108
+ "name": org.name,
109
+ "slug": org.slug,
110
+ "description": org.description,
111
+ "repository_url": org.repository_url,
112
+ "status": org.status,
113
+ })
114
+
115
+ except Exception as e:
116
+ return format_error("GET_ORG_ERROR", str(e), retry=False)
117
+
118
+ return asyncio.run(_get())
119
+
120
+
121
+ def orgs_create(
122
+ name: str,
123
+ slug: Optional[str] = None,
124
+ description: Optional[str] = None,
125
+ repository_url: Optional[str] = None,
126
+ status: str = "Active",
127
+ ) -> str:
128
+ """
129
+ Create a new organization.
130
+
131
+ Args:
132
+ name: Organization name
133
+ slug: URL-safe identifier (optional)
134
+ description: Organization description (optional)
135
+ repository_url: Code repository URL (optional)
136
+ status: Organization status (default: Active)
137
+
138
+ Example:
139
+ $ notion orgs create "My Organization" --slug "my-org"
140
+ """
141
+ async def _create() -> str:
142
+ try:
143
+ client = get_client()
144
+
145
+ # Register SDK plugin
146
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
147
+ plugin = AgentsSDKPlugin()
148
+ plugin.initialize(client)
149
+ client.register_sdk_plugin(plugin)
150
+
151
+ # Create organization
152
+ manager = client.plugin_manager("organizations")
153
+ org = await manager.create(
154
+ name=name,
155
+ slug=slug,
156
+ description=description,
157
+ repository_url=repository_url,
158
+ status=status,
159
+ )
160
+
161
+ return format_success({
162
+ "message": "Organization created successfully",
163
+ "id": org.id,
164
+ "name": org.name,
165
+ "slug": org.slug,
166
+ })
167
+
168
+ except Exception as e:
169
+ return format_error("CREATE_ORG_ERROR", str(e), retry=False)
170
+
171
+ return asyncio.run(_create())
172
+
173
+
174
+ # ===== PROJECTS =====
175
+
176
+ def projects_list(
177
+ org_id: Optional[str] = typer.Option(None, "--org-id", "-o", help="Filter by organization ID"),
178
+ ) -> str:
179
+ """
180
+ List all projects, optionally filtered by organization.
181
+
182
+ Args:
183
+ org_id: Filter by organization ID (optional)
184
+
185
+ Example:
186
+ $ notion projects list
187
+ $ notion projects list --org-id org_123
188
+ """
189
+ async def _list() -> str:
190
+ try:
191
+ client = get_client()
192
+
193
+ # Register SDK plugin
194
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
195
+ plugin = AgentsSDKPlugin()
196
+ plugin.initialize(client)
197
+ client.register_sdk_plugin(plugin)
198
+
199
+ # Get manager
200
+ manager = client.plugin_manager("projects")
201
+ projects = await manager.list(organization_id=org_id)
202
+
203
+ return format_success({
204
+ "projects": [
205
+ {
206
+ "id": p.id,
207
+ "name": p.name,
208
+ "slug": p.slug,
209
+ "status": p.status,
210
+ "role": p.role,
211
+ "organization_id": p.organization_id,
212
+ }
213
+ for p in projects
214
+ ],
215
+ "total": len(projects),
216
+ })
217
+
218
+ except Exception as e:
219
+ return format_error("LIST_PROJECTS_ERROR", str(e), retry=False)
220
+
221
+ return asyncio.run(_list())
222
+
223
+
224
+ def projects_get(project_id: str) -> str:
225
+ """
226
+ Get a project by ID.
227
+
228
+ Args:
229
+ project_id: Project page ID
230
+
231
+ Example:
232
+ $ notion projects get proj_123
233
+ """
234
+ async def _get() -> str:
235
+ try:
236
+ client = get_client()
237
+
238
+ # Register SDK plugin
239
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
240
+ plugin = AgentsSDKPlugin()
241
+ plugin.initialize(client)
242
+ client.register_sdk_plugin(plugin)
243
+
244
+ # Get project
245
+ manager = client.plugin_manager("projects")
246
+ project = await manager.get(project_id)
247
+
248
+ return format_success({
249
+ "id": project.id,
250
+ "name": project.name,
251
+ "slug": project.slug,
252
+ "description": project.description,
253
+ "repository": project.repository,
254
+ "status": project.status,
255
+ "tech_stack": project.tech_stack,
256
+ "role": project.role,
257
+ "organization_id": project.organization_id,
258
+ })
259
+
260
+ except Exception as e:
261
+ return format_error("GET_PROJECT_ERROR", str(e), retry=False)
262
+
263
+ return asyncio.run(_get())
264
+
265
+
266
+ def projects_create(
267
+ name: str,
268
+ organization_id: str,
269
+ slug: Optional[str] = typer.Option(None, "--slug", "-s", help="URL-safe identifier"),
270
+ description: Optional[str] = typer.Option(None, "--description", "-d", help="Project description"),
271
+ repository: Optional[str] = typer.Option(None, "--repository", "-r", help="Git repository URL"),
272
+ status: str = typer.Option("Active", "--status", help="Project status"),
273
+ tech_stack: Optional[str] = typer.Option(None, "--tech-stack", "-t", help="Comma-separated tech stack"),
274
+ role: str = typer.Option("Developer", "--role", help="Project role"),
275
+ ) -> str:
276
+ """
277
+ Create a new project.
278
+
279
+ Args:
280
+ name: Project name
281
+ organization_id: Parent organization ID
282
+ slug: URL-safe identifier (optional)
283
+ description: Project description (optional)
284
+ repository: Git repository URL (optional)
285
+ status: Project status (default: Active)
286
+ tech_stack: Comma-separated technologies (optional)
287
+ role: Project role (default: Developer)
288
+
289
+ Example:
290
+ $ notion projects create "My Project" org_123 --tech-stack "Python,React"
291
+ """
292
+ async def _create() -> str:
293
+ try:
294
+ client = get_client()
295
+
296
+ # Register SDK plugin
297
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
298
+ plugin = AgentsSDKPlugin()
299
+ plugin.initialize(client)
300
+ client.register_sdk_plugin(plugin)
301
+
302
+ # Parse tech stack
303
+ tech_stack_list = tech_stack.split(",") if tech_stack else None
304
+
305
+ # Create project
306
+ manager = client.plugin_manager("projects")
307
+ project = await manager.create(
308
+ name=name,
309
+ organization_id=organization_id,
310
+ slug=slug,
311
+ description=description,
312
+ repository=repository,
313
+ status=status,
314
+ tech_stack=tech_stack_list,
315
+ role=role,
316
+ )
317
+
318
+ return format_success({
319
+ "message": "Project created successfully",
320
+ "id": project.id,
321
+ "name": project.name,
322
+ "slug": project.slug,
323
+ })
324
+
325
+ except Exception as e:
326
+ return format_error("CREATE_PROJECT_ERROR", str(e), retry=False)
327
+
328
+ return asyncio.run(_create())
329
+
330
+
331
+ # ===== VERSIONS =====
332
+
333
+ def versions_list(
334
+ project_id: Optional[str] = typer.Option(None, "--project-id", "-p", help="Filter by project ID"),
335
+ ) -> str:
336
+ """
337
+ List all versions, optionally filtered by project.
338
+
339
+ Args:
340
+ project_id: Filter by project ID (optional)
341
+
342
+ Example:
343
+ $ notion versions list
344
+ $ notion versions list --project-id proj_123
345
+ """
346
+ async def _list() -> str:
347
+ try:
348
+ client = get_client()
349
+
350
+ # Register SDK plugin
351
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
352
+ plugin = AgentsSDKPlugin()
353
+ plugin.initialize(client)
354
+ client.register_sdk_plugin(plugin)
355
+
356
+ # Get manager
357
+ manager = client.plugin_manager("versions")
358
+ versions = await manager.list(project_id=project_id)
359
+
360
+ return format_success({
361
+ "versions": [
362
+ {
363
+ "id": v.id,
364
+ "name": v.name,
365
+ "status": v.status,
366
+ "type": v.version_type,
367
+ "branch_name": v.branch_name,
368
+ "progress": v.progress,
369
+ "project_id": v.project_id,
370
+ }
371
+ for v in versions
372
+ ],
373
+ "total": len(versions),
374
+ })
375
+
376
+ except Exception as e:
377
+ return format_error("LIST_VERSIONS_ERROR", str(e), retry=False)
378
+
379
+ return asyncio.run(_list())
380
+
381
+
382
+ def versions_get(version_id: str) -> str:
383
+ """
384
+ Get a version by ID.
385
+
386
+ Args:
387
+ version_id: Version page ID
388
+
389
+ Example:
390
+ $ notion versions get ver_123
391
+ """
392
+ async def _get() -> str:
393
+ try:
394
+ client = get_client()
395
+
396
+ # Register SDK plugin
397
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
398
+ plugin = AgentsSDKPlugin()
399
+ plugin.initialize(client)
400
+ client.register_sdk_plugin(plugin)
401
+
402
+ # Get version
403
+ manager = client.plugin_manager("versions")
404
+ version = await manager.get(version_id)
405
+
406
+ return format_success({
407
+ "id": version.id,
408
+ "name": version.name,
409
+ "status": version.status,
410
+ "type": version.version_type,
411
+ "branch_name": version.branch_name,
412
+ "progress": version.progress,
413
+ "project_id": version.project_id,
414
+ })
415
+
416
+ except Exception as e:
417
+ return format_error("GET_VERSION_ERROR", str(e), retry=False)
418
+
419
+ return asyncio.run(_get())
420
+
421
+
422
+ def versions_create(
423
+ name: str,
424
+ project_id: str,
425
+ status: str = typer.Option("Planning", "--status", help="Version status"),
426
+ version_type: str = typer.Option("Minor", "--type", help="Version type"),
427
+ branch_name: Optional[str] = typer.Option(None, "--branch", "-b", help="Git branch name"),
428
+ progress: int = typer.Option(0, "--progress", "-p", help="Progress percentage (0-100)"),
429
+ ) -> str:
430
+ """
431
+ Create a new version.
432
+
433
+ Args:
434
+ name: Version name (e.g., v1.0.0)
435
+ project_id: Parent project ID
436
+ status: Version status (default: Planning)
437
+ version_type: Version type (default: Minor)
438
+ branch_name: Git branch name (optional)
439
+ progress: Progress percentage 0-100 (default: 0)
440
+
441
+ Example:
442
+ $ notion versions create "v1.0.0" proj_123 --type Major
443
+ """
444
+ async def _create() -> str:
445
+ try:
446
+ client = get_client()
447
+
448
+ # Register SDK plugin
449
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
450
+ plugin = AgentsSDKPlugin()
451
+ plugin.initialize(client)
452
+ client.register_sdk_plugin(plugin)
453
+
454
+ # Create version
455
+ manager = client.plugin_manager("versions")
456
+ version = await manager.create(
457
+ name=name,
458
+ project_id=project_id,
459
+ status=status,
460
+ version_type=version_type,
461
+ branch_name=branch_name,
462
+ progress=progress,
463
+ )
464
+
465
+ return format_success({
466
+ "message": "Version created successfully",
467
+ "id": version.id,
468
+ "name": version.name,
469
+ })
470
+
471
+ except Exception as e:
472
+ return format_error("CREATE_VERSION_ERROR", str(e), retry=False)
473
+
474
+ return asyncio.run(_create())
475
+
476
+
477
+ # ===== TASKS =====
478
+
479
+ def tasks_list(
480
+ version_id: Optional[str] = typer.Option(None, "--version-id", "-v", help="Filter by version ID"),
481
+ status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status"),
482
+ ) -> str:
483
+ """
484
+ List all tasks, optionally filtered.
485
+
486
+ Args:
487
+ version_id: Filter by version ID (optional)
488
+ status: Filter by status (optional)
489
+
490
+ Example:
491
+ $ notion tasks list
492
+ $ notion tasks list --version-id ver_123 --status Backlog
493
+ """
494
+ async def _list() -> str:
495
+ try:
496
+ client = get_client()
497
+
498
+ # Register SDK plugin
499
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
500
+ plugin = AgentsSDKPlugin()
501
+ plugin.initialize(client)
502
+ client.register_sdk_plugin(plugin)
503
+
504
+ # Get manager
505
+ manager = client.plugin_manager("tasks")
506
+ tasks = await manager.list(version_id=version_id, status=status)
507
+
508
+ return format_success({
509
+ "tasks": [
510
+ {
511
+ "id": t.id,
512
+ "title": t.title,
513
+ "status": t.status,
514
+ "type": t.task_type,
515
+ "priority": t.priority,
516
+ "version_id": t.version_id,
517
+ "estimated_hours": t.estimated_hours,
518
+ }
519
+ for t in tasks
520
+ ],
521
+ "total": len(tasks),
522
+ })
523
+
524
+ except Exception as e:
525
+ return format_error("LIST_TASKS_ERROR", str(e), retry=False)
526
+
527
+ return asyncio.run(_list())
528
+
529
+
530
+ def tasks_get(task_id: str) -> str:
531
+ """
532
+ Get a task by ID.
533
+
534
+ Args:
535
+ task_id: Task page ID
536
+
537
+ Example:
538
+ $ notion tasks get task_123
539
+ """
540
+ async def _get() -> str:
541
+ try:
542
+ client = get_client()
543
+
544
+ # Register SDK plugin
545
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
546
+ plugin = AgentsSDKPlugin()
547
+ plugin.initialize(client)
548
+ client.register_sdk_plugin(plugin)
549
+
550
+ # Get task
551
+ manager = client.plugin_manager("tasks")
552
+ task = await manager.get(task_id)
553
+
554
+ return format_success({
555
+ "id": task.id,
556
+ "title": task.title,
557
+ "status": task.status,
558
+ "type": task.task_type,
559
+ "priority": task.priority,
560
+ "version_id": task.version_id,
561
+ "dependency_ids": task.dependency_ids,
562
+ "estimated_hours": task.estimated_hours,
563
+ "actual_hours": task.actual_hours,
564
+ })
565
+
566
+ except Exception as e:
567
+ return format_error("GET_TASK_ERROR", str(e), retry=False)
568
+
569
+ return asyncio.run(_get())
570
+
571
+
572
+ def tasks_create(
573
+ title: str,
574
+ version_id: str,
575
+ status: str = typer.Option("Backlog", "--status", help="Task status"),
576
+ task_type: str = typer.Option("New Feature", "--type", help="Task type"),
577
+ priority: str = typer.Option("Medium", "--priority", "-p", help="Task priority"),
578
+ dependencies: Optional[str] = typer.Option(None, "--dependencies", "-d", help="Comma-separated dependency task IDs"),
579
+ estimated_hours: Optional[int] = typer.Option(None, "--estimate", "-e", help="Estimated hours"),
580
+ ) -> str:
581
+ """
582
+ Create a new task.
583
+
584
+ Args:
585
+ title: Task title
586
+ version_id: Parent version ID
587
+ status: Task status (default: Backlog)
588
+ task_type: Task type (default: New Feature)
589
+ priority: Task priority (default: Medium)
590
+ dependencies: Comma-separated dependency task IDs (optional)
591
+ estimated_hours: Estimated hours (optional)
592
+
593
+ Example:
594
+ $ notion tasks create "Fix authentication bug" ver_123 --priority High --type "Bug Fix"
595
+ """
596
+ async def _create() -> str:
597
+ try:
598
+ client = get_client()
599
+
600
+ # Register SDK plugin
601
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
602
+ plugin = AgentsSDKPlugin()
603
+ plugin.initialize(client)
604
+ client.register_sdk_plugin(plugin)
605
+
606
+ # Parse dependencies
607
+ dependency_ids = dependencies.split(",") if dependencies else None
608
+
609
+ # Create task
610
+ manager = client.plugin_manager("tasks")
611
+ task = await manager.create(
612
+ title=title,
613
+ version_id=version_id,
614
+ status=status,
615
+ task_type=task_type,
616
+ priority=priority,
617
+ dependency_ids=dependency_ids,
618
+ estimated_hours=estimated_hours,
619
+ )
620
+
621
+ return format_success({
622
+ "message": "Task created successfully",
623
+ "id": task.id,
624
+ "title": task.title,
625
+ "status": task.status,
626
+ })
627
+
628
+ except Exception as e:
629
+ return format_error("CREATE_TASK_ERROR", str(e), retry=False)
630
+
631
+ return asyncio.run(_create())
632
+
633
+
634
+ # ===== TASK WORKFLOW COMMANDS =====
635
+
636
+ def tasks_next(
637
+ project_id: Optional[str] = typer.Option(None, "--project-id", "-p", help="Filter by project ID"),
638
+ ) -> str:
639
+ """
640
+ Find the next available task to work on.
641
+
642
+ Finds a task that is:
643
+ - In Backlog or Claimed status
644
+ - Has all dependencies completed
645
+
646
+ Args:
647
+ project_id: Filter by project ID (optional)
648
+
649
+ Example:
650
+ $ notion tasks next
651
+ $ notion tasks next --project-id proj_123
652
+ """
653
+ async def _next() -> str:
654
+ try:
655
+ client = get_client()
656
+
657
+ # Register SDK plugin
658
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
659
+ plugin = AgentsSDKPlugin()
660
+ plugin.initialize(client)
661
+ client.register_sdk_plugin(plugin)
662
+
663
+ # Get manager
664
+ manager = client.plugin_manager("tasks")
665
+ task = await manager.next(project_id=project_id)
666
+
667
+ if not task:
668
+ return format_success({
669
+ "message": "No available tasks found",
670
+ "task": None,
671
+ })
672
+
673
+ return format_success({
674
+ "message": "Found available task",
675
+ "task": {
676
+ "id": task.id,
677
+ "title": task.title,
678
+ "status": task.status,
679
+ "priority": task.priority,
680
+ "version_id": task.version_id,
681
+ "can_start": True,
682
+ },
683
+ })
684
+
685
+ except Exception as e:
686
+ return format_error("FIND_NEXT_TASK_ERROR", str(e), retry=False)
687
+
688
+ return asyncio.run(_next())
689
+
690
+
691
+ def tasks_claim(task_id: str) -> str:
692
+ """
693
+ Claim a task (transition to Claimed status).
694
+
695
+ Args:
696
+ task_id: Task page ID
697
+
698
+ Example:
699
+ $ notion tasks claim task_123
700
+ """
701
+ async def _claim() -> str:
702
+ try:
703
+ client = get_client()
704
+
705
+ # Register SDK plugin
706
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
707
+ plugin = AgentsSDKPlugin()
708
+ plugin.initialize(client)
709
+ client.register_sdk_plugin(plugin)
710
+
711
+ # Get and claim task
712
+ manager = client.plugin_manager("tasks")
713
+ task = await manager.get(task_id)
714
+ await task.claim()
715
+
716
+ # Get agent ID for tracking
717
+ agent_id = get_or_create_agent_id()
718
+
719
+ return format_success({
720
+ "message": f"Task claimed by agent {agent_id}",
721
+ "task_id": task.id,
722
+ "title": task.title,
723
+ "status": task.status,
724
+ "agent_id": agent_id,
725
+ })
726
+
727
+ except Exception as e:
728
+ return format_error("CLAIM_TASK_ERROR", str(e), retry=False)
729
+
730
+ return asyncio.run(_claim())
731
+
732
+
733
+ def tasks_start(task_id: str) -> str:
734
+ """
735
+ Start working on a task (transition to In Progress).
736
+
737
+ Args:
738
+ task_id: Task page ID
739
+
740
+ Example:
741
+ $ notion tasks start task_123
742
+ """
743
+ async def _start() -> str:
744
+ try:
745
+ client = get_client()
746
+
747
+ # Register SDK plugin
748
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
749
+ plugin = AgentsSDKPlugin()
750
+ plugin.initialize(client)
751
+ client.register_sdk_plugin(plugin)
752
+
753
+ # Get and start task
754
+ manager = client.plugin_manager("tasks")
755
+ task = await manager.get(task_id)
756
+
757
+ # Check if can start
758
+ if not await task.can_start():
759
+ return format_error(
760
+ "TASK_BLOCKED",
761
+ "Task has incomplete dependencies",
762
+ retry=False,
763
+ )
764
+
765
+ await task.start()
766
+
767
+ # Get agent ID for tracking
768
+ agent_id = get_or_create_agent_id()
769
+
770
+ return format_success({
771
+ "message": f"Task started by agent {agent_id}",
772
+ "task_id": task.id,
773
+ "title": task.title,
774
+ "status": task.status,
775
+ "agent_id": agent_id,
776
+ })
777
+
778
+ except Exception as e:
779
+ return format_error("START_TASK_ERROR", str(e), retry=False)
780
+
781
+ return asyncio.run(_start())
782
+
783
+
784
+ def tasks_complete(
785
+ task_id: str,
786
+ actual_hours: Optional[int] = typer.Option(None, "--actual-hours", "-a", help="Actual hours spent"),
787
+ ) -> str:
788
+ """
789
+ Complete a task (transition to Completed).
790
+
791
+ Args:
792
+ task_id: Task page ID
793
+ actual_hours: Actual hours spent (optional)
794
+
795
+ Example:
796
+ $ notion tasks complete task_123 --actual-hours 3
797
+ """
798
+ async def _complete() -> str:
799
+ try:
800
+ client = get_client()
801
+
802
+ # Register SDK plugin
803
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
804
+ plugin = AgentsSDKPlugin()
805
+ plugin.initialize(client)
806
+ client.register_sdk_plugin(plugin)
807
+
808
+ # Get and complete task
809
+ manager = client.plugin_manager("tasks")
810
+ task = await manager.get(task_id)
811
+ await task.complete(actual_hours=actual_hours)
812
+
813
+ # Get agent ID for tracking
814
+ agent_id = get_or_create_agent_id()
815
+
816
+ return format_success({
817
+ "message": f"Task completed by agent {agent_id}",
818
+ "task_id": task.id,
819
+ "title": task.title,
820
+ "status": task.status,
821
+ "actual_hours": task.actual_hours,
822
+ "agent_id": agent_id,
823
+ })
824
+
825
+ except Exception as e:
826
+ return format_error("COMPLETE_TASK_ERROR", str(e), retry=False)
827
+
828
+ return asyncio.run(_complete())
829
+
830
+
831
+ def tasks_can_start(task_id: str) -> str:
832
+ """
833
+ Check if a task can start (all dependencies completed).
834
+
835
+ Args:
836
+ task_id: Task page ID
837
+
838
+ Example:
839
+ $ notion tasks can-start task_123
840
+ """
841
+ async def _can_start() -> str:
842
+ try:
843
+ client = get_client()
844
+
845
+ # Register SDK plugin
846
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
847
+ plugin = AgentsSDKPlugin()
848
+ plugin.initialize(client)
849
+ client.register_sdk_plugin(plugin)
850
+
851
+ # Get task and check
852
+ manager = client.plugin_manager("tasks")
853
+ task = await manager.get(task_id)
854
+ can_start = await task.can_start()
855
+
856
+ if not can_start:
857
+ # Get incomplete dependencies
858
+ incomplete = []
859
+ for dep in await task.dependencies():
860
+ if dep.status != "Completed":
861
+ incomplete.append({
862
+ "id": dep.id,
863
+ "title": dep.title,
864
+ "status": dep.status,
865
+ })
866
+
867
+ return format_success({
868
+ "task_id": task.id,
869
+ "can_start": False,
870
+ "incomplete_dependencies": incomplete,
871
+ })
872
+
873
+ return format_success({
874
+ "task_id": task.id,
875
+ "can_start": True,
876
+ "message": "All dependencies are completed",
877
+ })
878
+
879
+ except Exception as e:
880
+ return format_error("CAN_START_ERROR", str(e), retry=False)
881
+
882
+ return asyncio.run(_can_start())
883
+
884
+
885
+ # ===== IDEAS =====
886
+
887
+ def ideas_list(
888
+ project_id: Optional[str] = None,
889
+ category: Optional[str] = None,
890
+ status: Optional[str] = None,
891
+ effort_estimate: Optional[str] = None,
892
+ ) -> str:
893
+ """
894
+ List ideas with optional filtering.
895
+
896
+ Args:
897
+ project_id: Filter by project ID
898
+ category: Filter by category
899
+ status: Filter by status
900
+ effort_estimate: Filter by effort estimate
901
+
902
+ Example:
903
+ $ notion ideas list --project-id proj_123 --status Proposed
904
+ """
905
+ async def _list() -> str:
906
+ try:
907
+ client = get_client()
908
+
909
+ # Register SDK plugin
910
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
911
+ plugin = AgentsSDKPlugin()
912
+ plugin.initialize(client)
913
+ client.register_sdk_plugin(plugin)
914
+
915
+ # Get manager and list
916
+ manager = client.plugin_manager("ideas")
917
+ ideas = await manager.list(
918
+ project_id=project_id,
919
+ category=category,
920
+ status=status,
921
+ effort_estimate=effort_estimate,
922
+ )
923
+
924
+ return format_success({
925
+ "ideas": [
926
+ {
927
+ "id": idea.id,
928
+ "title": idea.title,
929
+ "category": idea.category,
930
+ "status": idea.status,
931
+ "effort_estimate": idea.effort_estimate,
932
+ }
933
+ for idea in ideas
934
+ ],
935
+ "total": len(ideas),
936
+ })
937
+
938
+ except Exception as e:
939
+ return format_error("LIST_IDEAS_ERROR", str(e), retry=False)
940
+
941
+ return asyncio.run(_list())
942
+
943
+
944
+ def ideas_get(idea_id: str) -> str:
945
+ """
946
+ Get an idea by ID.
947
+
948
+ Args:
949
+ idea_id: Idea page ID
950
+
951
+ Example:
952
+ $ notion ideas get idea_123
953
+ """
954
+ async def _get() -> str:
955
+ try:
956
+ client = get_client()
957
+
958
+ # Register SDK plugin
959
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
960
+ plugin = AgentsSDKPlugin()
961
+ plugin.initialize(client)
962
+ client.register_sdk_plugin(plugin)
963
+
964
+ # Get idea
965
+ from better_notion.plugins.official.agents_sdk.models import Idea
966
+ idea = await Idea.get(idea_id, client=client)
967
+
968
+ return format_success({
969
+ "id": idea.id,
970
+ "title": idea.title,
971
+ "category": idea.category,
972
+ "status": idea.status,
973
+ "description": idea.description,
974
+ "effort_estimate": idea.effort_estimate,
975
+ "project_id": idea.project_id,
976
+ })
977
+
978
+ except Exception as e:
979
+ return format_error("GET_IDEA_ERROR", str(e), retry=False)
980
+
981
+ return asyncio.run(_get())
982
+
983
+
984
+ def ideas_create(
985
+ title: str,
986
+ project_id: str,
987
+ category: str = "enhancement",
988
+ description: str = "",
989
+ proposed_solution: str = "",
990
+ benefits: str = "",
991
+ effort_estimate: str = "M",
992
+ context: str = "",
993
+ ) -> str:
994
+ """
995
+ Create a new idea.
996
+
997
+ Args:
998
+ title: Idea title
999
+ project_id: Project ID
1000
+ category: Idea category (enhancement, feature, bugfix, optimization, research)
1001
+ description: Detailed description
1002
+ proposed_solution: Proposed solution
1003
+ benefits: Expected benefits
1004
+ effort_estimate: Effort estimate (XS, S, M, L, XL)
1005
+ context: Additional context
1006
+
1007
+ Example:
1008
+ $ notion ideas create "Add caching" --project-id proj_123 \\
1009
+ --category enhancement --effort-estimate M
1010
+ """
1011
+ async def _create() -> str:
1012
+ try:
1013
+ client = get_client()
1014
+
1015
+ # Register SDK plugin
1016
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1017
+ plugin = AgentsSDKPlugin()
1018
+ plugin.initialize(client)
1019
+ client.register_sdk_plugin(plugin)
1020
+
1021
+ # Get workspace config
1022
+ workspace_config = get_workspace_config()
1023
+ database_id = workspace_config.get("Ideas")
1024
+
1025
+ if not database_id:
1026
+ return format_error("NO_DATABASE", "Ideas database not configured", retry=False)
1027
+
1028
+ # Create idea in Notion
1029
+ properties = {
1030
+ "title": {"title": [{"text": {"content": title}}]},
1031
+ "project_id": {"relation": [{"id": project_id}]},
1032
+ "category": {"select": {"name": category}},
1033
+ "status": {"select": {"name": "Proposed"}},
1034
+ "effort_estimate": {"select": {"name": effort_estimate}},
1035
+ }
1036
+
1037
+ if description:
1038
+ properties["description"] = {
1039
+ "rich_text": [{"text": {"content": description}}]
1040
+ }
1041
+
1042
+ if proposed_solution:
1043
+ properties["proposed_solution"] = {
1044
+ "rich_text": [{"text": {"content": proposed_solution}}]
1045
+ }
1046
+
1047
+ if benefits:
1048
+ properties["benefits"] = {
1049
+ "rich_text": [{"text": {"content": benefits}}]
1050
+ }
1051
+
1052
+ if context:
1053
+ properties["context"] = {
1054
+ "rich_text": [{"text": {"content": context}}]
1055
+ }
1056
+
1057
+ response = await client._api.request(
1058
+ method="POST",
1059
+ path=f"databases/{database_id}",
1060
+ json={"properties": properties},
1061
+ )
1062
+
1063
+ return format_success({
1064
+ "message": "Idea created successfully",
1065
+ "idea_id": response["id"],
1066
+ "title": title,
1067
+ "category": category,
1068
+ })
1069
+
1070
+ except Exception as e:
1071
+ return format_error("CREATE_IDEA_ERROR", str(e), retry=False)
1072
+
1073
+ return asyncio.run(_create())
1074
+
1075
+
1076
+ def ideas_review(count: int = 10) -> str:
1077
+ """
1078
+ Get a batch of ideas for review, prioritized by effort.
1079
+
1080
+ Args:
1081
+ count: Maximum number of ideas to return
1082
+
1083
+ Example:
1084
+ $ notion ideas review --count 5
1085
+ """
1086
+ async def _review() -> str:
1087
+ try:
1088
+ client = get_client()
1089
+
1090
+ # Register SDK plugin
1091
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1092
+ plugin = AgentsSDKPlugin()
1093
+ plugin.initialize(client)
1094
+ client.register_sdk_plugin(plugin)
1095
+
1096
+ # Get manager and review batch
1097
+ manager = client.plugin_manager("ideas")
1098
+ ideas = await manager.review_batch(count=count)
1099
+
1100
+ return format_success({
1101
+ "ideas_for_review": [
1102
+ {
1103
+ "id": idea.id,
1104
+ "title": idea.title,
1105
+ "category": idea.category,
1106
+ "effort_estimate": idea.effort_estimate,
1107
+ "status": idea.status,
1108
+ }
1109
+ for idea in ideas
1110
+ ],
1111
+ "total": len(ideas),
1112
+ })
1113
+
1114
+ except Exception as e:
1115
+ return format_error("REVIEW_IDEAS_ERROR", str(e), retry=False)
1116
+
1117
+ return asyncio.run(_review())
1118
+
1119
+
1120
+ def ideas_accept(idea_id: str) -> str:
1121
+ """
1122
+ Accept an idea (moves to Accepted status).
1123
+
1124
+ Args:
1125
+ idea_id: Idea page ID
1126
+
1127
+ Example:
1128
+ $ notion ideas accept idea_123
1129
+ """
1130
+ async def _accept() -> str:
1131
+ try:
1132
+ client = get_client()
1133
+
1134
+ # Register SDK plugin
1135
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1136
+ plugin = AgentsSDKPlugin()
1137
+ plugin.initialize(client)
1138
+ client.register_sdk_plugin(plugin)
1139
+
1140
+ # Get idea and accept
1141
+ from better_notion.plugins.official.agents_sdk.models import Idea
1142
+ idea = await Idea.get(idea_id, client=client)
1143
+ await idea.accept()
1144
+
1145
+ return format_success({
1146
+ "message": "Idea accepted successfully",
1147
+ "idea_id": idea.id,
1148
+ "status": idea.status,
1149
+ })
1150
+
1151
+ except Exception as e:
1152
+ return format_error("ACCEPT_IDEA_ERROR", str(e), retry=False)
1153
+
1154
+ return asyncio.run(_accept())
1155
+
1156
+
1157
+ def ideas_reject(idea_id: str, reason: str = "") -> str:
1158
+ """
1159
+ Reject an idea (moves to Rejected status).
1160
+
1161
+ Args:
1162
+ idea_id: Idea page ID
1163
+ reason: Reason for rejection
1164
+
1165
+ Example:
1166
+ $ notion ideas reject idea_123 --reason "Out of scope"
1167
+ """
1168
+ async def _reject() -> str:
1169
+ try:
1170
+ client = get_client()
1171
+
1172
+ # Register SDK plugin
1173
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1174
+ plugin = AgentsSDKPlugin()
1175
+ plugin.initialize(client)
1176
+ client.register_sdk_plugin(plugin)
1177
+
1178
+ # Get idea and reject
1179
+ from better_notion.plugins.official.agents_sdk.models import Idea
1180
+ idea = await Idea.get(idea_id, client=client)
1181
+ await idea.reject()
1182
+
1183
+ return format_success({
1184
+ "message": "Idea rejected successfully",
1185
+ "idea_id": idea.id,
1186
+ "status": idea.status,
1187
+ "reason": reason,
1188
+ })
1189
+
1190
+ except Exception as e:
1191
+ return format_error("REJECT_IDEA_ERROR", str(e), retry=False)
1192
+
1193
+ return asyncio.run(_reject())
1194
+
1195
+
1196
+ # ===== WORK ISSUES =====
1197
+
1198
+ def work_issues_list(
1199
+ project_id: Optional[str] = None,
1200
+ task_id: Optional[str] = None,
1201
+ type: Optional[str] = None,
1202
+ severity: Optional[str] = None,
1203
+ status: Optional[str] = None,
1204
+ ) -> str:
1205
+ """
1206
+ List work issues with optional filtering.
1207
+
1208
+ Args:
1209
+ project_id: Filter by project ID
1210
+ task_id: Filter by task ID
1211
+ type: Filter by type
1212
+ severity: Filter by severity
1213
+ status: Filter by status
1214
+
1215
+ Example:
1216
+ $ notion work-issues list --project-id proj_123 --severity High
1217
+ """
1218
+ async def _list() -> str:
1219
+ try:
1220
+ client = get_client()
1221
+
1222
+ # Register SDK plugin
1223
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1224
+ plugin = AgentsSDKPlugin()
1225
+ plugin.initialize(client)
1226
+ client.register_sdk_plugin(plugin)
1227
+
1228
+ # Get manager and list
1229
+ manager = client.plugin_manager("work_issues")
1230
+ issues = await manager.list(
1231
+ project_id=project_id,
1232
+ task_id=task_id,
1233
+ type_=type,
1234
+ severity=severity,
1235
+ status=status,
1236
+ )
1237
+
1238
+ return format_success({
1239
+ "work_issues": [
1240
+ {
1241
+ "id": issue.id,
1242
+ "title": issue.title,
1243
+ "type": issue.type,
1244
+ "severity": issue.severity,
1245
+ "status": issue.status,
1246
+ }
1247
+ for issue in issues
1248
+ ],
1249
+ "total": len(issues),
1250
+ })
1251
+
1252
+ except Exception as e:
1253
+ return format_error("LIST_WORK_ISSUES_ERROR", str(e), retry=False)
1254
+
1255
+ return asyncio.run(_list())
1256
+
1257
+
1258
+ def work_issues_get(issue_id: str) -> str:
1259
+ """
1260
+ Get a work issue by ID.
1261
+
1262
+ Args:
1263
+ issue_id: Work issue page ID
1264
+
1265
+ Example:
1266
+ $ notion work-issues get issue_123
1267
+ """
1268
+ async def _get() -> str:
1269
+ try:
1270
+ client = get_client()
1271
+
1272
+ # Register SDK plugin
1273
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1274
+ plugin = AgentsSDKPlugin()
1275
+ plugin.initialize(client)
1276
+ client.register_sdk_plugin(plugin)
1277
+
1278
+ # Get issue
1279
+ from better_notion.plugins.official.agents_sdk.models import WorkIssue
1280
+ issue = await WorkIssue.get(issue_id, client=client)
1281
+
1282
+ return format_success({
1283
+ "id": issue.id,
1284
+ "title": issue.title,
1285
+ "type": issue.type,
1286
+ "severity": issue.severity,
1287
+ "status": issue.status,
1288
+ "description": issue.description,
1289
+ "project_id": issue.project_id,
1290
+ "task_id": issue.task_id,
1291
+ })
1292
+
1293
+ except Exception as e:
1294
+ return format_error("GET_WORK_ISSUE_ERROR", str(e), retry=False)
1295
+
1296
+ return asyncio.run(_get())
1297
+
1298
+
1299
+ def work_issues_create(
1300
+ title: str,
1301
+ project_id: str,
1302
+ type: str = "Technical",
1303
+ severity: str = "Medium",
1304
+ description: str = "",
1305
+ context: str = "",
1306
+ task_id: Optional[str] = None,
1307
+ ) -> str:
1308
+ """
1309
+ Create a new work issue.
1310
+
1311
+ Args:
1312
+ title: Issue title
1313
+ project_id: Project ID
1314
+ type: Issue type (Technical, Process, Resource, Other)
1315
+ severity: Issue severity (Low, Medium, High, Critical)
1316
+ description: Detailed description
1317
+ context: Additional context
1318
+ task_id: Related task ID (optional)
1319
+
1320
+ Example:
1321
+ $ notion work-issues create "API error" --project-id proj_123 \\
1322
+ --type Technical --severity High
1323
+ """
1324
+ async def _create() -> str:
1325
+ try:
1326
+ client = get_client()
1327
+
1328
+ # Register SDK plugin
1329
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1330
+ plugin = AgentsSDKPlugin()
1331
+ plugin.initialize(client)
1332
+ client.register_sdk_plugin(plugin)
1333
+
1334
+ # Get workspace config
1335
+ workspace_config = get_workspace_config()
1336
+ database_id = workspace_config.get("Work Issues")
1337
+
1338
+ if not database_id:
1339
+ return format_error("NO_DATABASE", "Work Issues database not configured", retry=False)
1340
+
1341
+ # Create issue in Notion
1342
+ properties = {
1343
+ "title": {"title": [{"text": {"content": title}}]},
1344
+ "project_id": {"relation": [{"id": project_id}]},
1345
+ "type": {"select": {"name": type}},
1346
+ "severity": {"select": {"name": severity}},
1347
+ "status": {"select": {"name": "Open"}},
1348
+ }
1349
+
1350
+ if description:
1351
+ properties["description"] = {
1352
+ "rich_text": [{"text": {"content": description}}]
1353
+ }
1354
+
1355
+ if context:
1356
+ properties["context"] = {
1357
+ "rich_text": [{"text": {"content": context}}]
1358
+ }
1359
+
1360
+ if task_id:
1361
+ properties["task_id"] = {"relation": [{"id": task_id}]}
1362
+
1363
+ response = await client._api.request(
1364
+ method="POST",
1365
+ path=f"databases/{database_id}",
1366
+ json={"properties": properties},
1367
+ )
1368
+
1369
+ return format_success({
1370
+ "message": "Work issue created successfully",
1371
+ "issue_id": response["id"],
1372
+ "title": title,
1373
+ "type": type,
1374
+ "severity": severity,
1375
+ })
1376
+
1377
+ except Exception as e:
1378
+ return format_error("CREATE_WORK_ISSUE_ERROR", str(e), retry=False)
1379
+
1380
+ return asyncio.run(_create())
1381
+
1382
+
1383
+ def work_issues_resolve(issue_id: str, resolution: str = "") -> str:
1384
+ """
1385
+ Resolve a work issue.
1386
+
1387
+ Args:
1388
+ issue_id: Work issue page ID
1389
+ resolution: Resolution description
1390
+
1391
+ Example:
1392
+ $ notion work-issues resolve issue_123 --resolution "Fixed dependency version"
1393
+ """
1394
+ async def _resolve() -> str:
1395
+ try:
1396
+ client = get_client()
1397
+
1398
+ # Register SDK plugin
1399
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1400
+ plugin = AgentsSDKPlugin()
1401
+ plugin.initialize(client)
1402
+ client.register_sdk_plugin(plugin)
1403
+
1404
+ # Get issue and resolve
1405
+ from better_notion.plugins.official.agents_sdk.models import WorkIssue
1406
+ issue = await WorkIssue.get(issue_id, client=client)
1407
+ await issue.resolve()
1408
+
1409
+ return format_success({
1410
+ "message": "Work issue resolved successfully",
1411
+ "issue_id": issue.id,
1412
+ "status": issue.status,
1413
+ "resolution": resolution,
1414
+ })
1415
+
1416
+ except Exception as e:
1417
+ return format_error("RESOLVE_WORK_ISSUE_ERROR", str(e), retry=False)
1418
+
1419
+ return asyncio.run(_resolve())
1420
+
1421
+
1422
+ def work_issues_blockers(project_id: str) -> str:
1423
+ """
1424
+ Find all blocking work issues for a project.
1425
+
1426
+ Args:
1427
+ project_id: Project ID
1428
+
1429
+ Example:
1430
+ $ notion work-issues blockers proj_123
1431
+ """
1432
+ async def _blockers() -> str:
1433
+ try:
1434
+ client = get_client()
1435
+
1436
+ # Register SDK plugin
1437
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1438
+ plugin = AgentsSDKPlugin()
1439
+ plugin.initialize(client)
1440
+ client.register_sdk_plugin(plugin)
1441
+
1442
+ # Get manager and find blockers
1443
+ manager = client.plugin_manager("work_issues")
1444
+ blockers = await manager.find_blockers(project_id)
1445
+
1446
+ return format_success({
1447
+ "blocking_issues": [
1448
+ {
1449
+ "id": issue.id,
1450
+ "title": issue.title,
1451
+ "type": issue.type,
1452
+ "severity": issue.severity,
1453
+ "status": issue.status,
1454
+ }
1455
+ for issue in blockers
1456
+ ],
1457
+ "total": len(blockers),
1458
+ })
1459
+
1460
+ except Exception as e:
1461
+ return format_error("FIND_BLOCKERS_ERROR", str(e), retry=False)
1462
+
1463
+ return asyncio.run(_blockers())
1464
+
1465
+
1466
+ # ===== INCIDENTS =====
1467
+
1468
+ def incidents_list(
1469
+ project_id: Optional[str] = None,
1470
+ version_id: Optional[str] = None,
1471
+ severity: Optional[str] = None,
1472
+ status: Optional[str] = None,
1473
+ ) -> str:
1474
+ """
1475
+ List incidents with optional filtering.
1476
+
1477
+ Args:
1478
+ project_id: Filter by project ID
1479
+ version_id: Filter by affected version ID
1480
+ severity: Filter by severity
1481
+ status: Filter by status
1482
+
1483
+ Example:
1484
+ $ notion incidents list --project-id proj_123 --severity Critical
1485
+ """
1486
+ async def _list() -> str:
1487
+ try:
1488
+ client = get_client()
1489
+
1490
+ # Register SDK plugin
1491
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1492
+ plugin = AgentsSDKPlugin()
1493
+ plugin.initialize(client)
1494
+ client.register_sdk_plugin(plugin)
1495
+
1496
+ # Get manager and list
1497
+ manager = client.plugin_manager("incidents")
1498
+ incidents = await manager.list(
1499
+ project_id=project_id,
1500
+ version_id=version_id,
1501
+ severity=severity,
1502
+ status=status,
1503
+ )
1504
+
1505
+ return format_success({
1506
+ "incidents": [
1507
+ {
1508
+ "id": incident.id,
1509
+ "title": incident.title,
1510
+ "severity": incident.severity,
1511
+ "status": incident.status,
1512
+ "type": incident.type,
1513
+ }
1514
+ for incident in incidents
1515
+ ],
1516
+ "total": len(incidents),
1517
+ })
1518
+
1519
+ except Exception as e:
1520
+ return format_error("LIST_INCIDENTS_ERROR", str(e), retry=False)
1521
+
1522
+ return asyncio.run(_list())
1523
+
1524
+
1525
+ def incidents_get(incident_id: str) -> str:
1526
+ """
1527
+ Get an incident by ID.
1528
+
1529
+ Args:
1530
+ incident_id: Incident page ID
1531
+
1532
+ Example:
1533
+ $ notion incidents get incident_123
1534
+ """
1535
+ async def _get() -> str:
1536
+ try:
1537
+ client = get_client()
1538
+
1539
+ # Register SDK plugin
1540
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1541
+ plugin = AgentsSDKPlugin()
1542
+ plugin.initialize(client)
1543
+ client.register_sdk_plugin(plugin)
1544
+
1545
+ # Get incident
1546
+ from better_notion.plugins.official.agents_sdk.models import Incident
1547
+ incident = await Incident.get(incident_id, client=client)
1548
+
1549
+ return format_success({
1550
+ "id": incident.id,
1551
+ "title": incident.title,
1552
+ "severity": incident.severity,
1553
+ "status": incident.status,
1554
+ "type": incident.type,
1555
+ "project_id": incident.project_id,
1556
+ "affected_version_id": incident.affected_version_id,
1557
+ "discovery_date": str(incident.discovery_date) if incident.discovery_date else None,
1558
+ "resolved_date": str(incident.resolved_date) if incident.resolved_date else None,
1559
+ })
1560
+
1561
+ except Exception as e:
1562
+ return format_error("GET_INCIDENT_ERROR", str(e), retry=False)
1563
+
1564
+ return asyncio.run(_get())
1565
+
1566
+
1567
+ def incidents_create(
1568
+ title: str,
1569
+ project_id: str,
1570
+ severity: str = "Medium",
1571
+ type: str = "Bug",
1572
+ affected_version_id: Optional[str] = None,
1573
+ root_cause: str = "",
1574
+ ) -> str:
1575
+ """
1576
+ Create a new incident.
1577
+
1578
+ Args:
1579
+ title: Incident title
1580
+ project_id: Project ID
1581
+ severity: Incident severity (Low, Medium, High, Critical)
1582
+ type: Incident type (Bug, Performance, Security, Data, Other)
1583
+ affected_version_id: Affected version ID
1584
+ root_cause: Root cause analysis
1585
+
1586
+ Example:
1587
+ $ notion incidents create "Production outage" --project-id proj_123 \\
1588
+ --severity Critical --type Bug
1589
+ """
1590
+ async def _create() -> str:
1591
+ try:
1592
+ client = get_client()
1593
+
1594
+ # Register SDK plugin
1595
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1596
+ plugin = AgentsSDKPlugin()
1597
+ plugin.initialize(client)
1598
+ client.register_sdk_plugin(plugin)
1599
+
1600
+ # Get workspace config
1601
+ workspace_config = get_workspace_config()
1602
+ database_id = workspace_config.get("Incidents")
1603
+
1604
+ if not database_id:
1605
+ return format_error("NO_DATABASE", "Incidents database not configured", retry=False)
1606
+
1607
+ # Create incident in Notion
1608
+ from datetime import datetime, timezone
1609
+
1610
+ properties = {
1611
+ "title": {"title": [{"text": {"content": title}}]},
1612
+ "project_id": {"relation": [{"id": project_id}]},
1613
+ "severity": {"select": {"name": severity}},
1614
+ "type": {"select": {"name": type}},
1615
+ "status": {"select": {"name": "Active"}},
1616
+ "discovery_date": {
1617
+ "date": {"start": datetime.now(timezone.utc).isoformat()}
1618
+ },
1619
+ }
1620
+
1621
+ if affected_version_id:
1622
+ properties["affected_version_id"] = {"relation": [{"id": affected_version_id}]}
1623
+
1624
+ if root_cause:
1625
+ properties["root_cause"] = {
1626
+ "rich_text": [{"text": {"content": root_cause}}]
1627
+ }
1628
+
1629
+ response = await client._api.request(
1630
+ method="POST",
1631
+ path=f"databases/{database_id}",
1632
+ json={"properties": properties},
1633
+ )
1634
+
1635
+ return format_success({
1636
+ "message": "Incident created successfully",
1637
+ "incident_id": response["id"],
1638
+ "title": title,
1639
+ "severity": severity,
1640
+ })
1641
+
1642
+ except Exception as e:
1643
+ return format_error("CREATE_INCIDENT_ERROR", str(e), retry=False)
1644
+
1645
+ return asyncio.run(_create())
1646
+
1647
+
1648
+ def incidents_resolve(incident_id: str, resolution: str = "") -> str:
1649
+ """
1650
+ Resolve an incident.
1651
+
1652
+ Args:
1653
+ incident_id: Incident page ID
1654
+ resolution: Resolution description
1655
+
1656
+ Example:
1657
+ $ notion incidents resolve incident_123 --resolution "Hotfix deployed"
1658
+ """
1659
+ async def _resolve() -> str:
1660
+ try:
1661
+ client = get_client()
1662
+
1663
+ # Register SDK plugin
1664
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1665
+ plugin = AgentsSDKPlugin()
1666
+ plugin.initialize(client)
1667
+ client.register_sdk_plugin(plugin)
1668
+
1669
+ # Get incident and resolve
1670
+ from better_notion.plugins.official.agents_sdk.models import Incident
1671
+ incident = await Incident.get(incident_id, client=client)
1672
+ await incident.resolve()
1673
+
1674
+ return format_success({
1675
+ "message": "Incident resolved successfully",
1676
+ "incident_id": incident.id,
1677
+ "status": incident.status,
1678
+ "resolution": resolution,
1679
+ })
1680
+
1681
+ except Exception as e:
1682
+ return format_error("RESOLVE_INCIDENT_ERROR", str(e), retry=False)
1683
+
1684
+ return asyncio.run(_resolve())
1685
+
1686
+
1687
+ def incidents_mttr(project_id: Optional[str] = None, within_days: int = 30) -> str:
1688
+ """
1689
+ Calculate Mean Time To Resolve (MTTR) for incidents.
1690
+
1691
+ Args:
1692
+ project_id: Filter by project ID (optional)
1693
+ within_days: Only consider incidents from last N days
1694
+
1695
+ Example:
1696
+ $ notion incidents mttr --project-id proj_123 --within-days 30
1697
+ """
1698
+ async def _mttr() -> str:
1699
+ try:
1700
+ client = get_client()
1701
+
1702
+ # Register SDK plugin
1703
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1704
+ plugin = AgentsSDKPlugin()
1705
+ plugin.initialize(client)
1706
+ client.register_sdk_plugin(plugin)
1707
+
1708
+ # Get manager and calculate MTTR
1709
+ manager = client.plugin_manager("incidents")
1710
+ mttr = await manager.calculate_mttr(
1711
+ project_id=project_id,
1712
+ within_days=within_days,
1713
+ )
1714
+
1715
+ return format_success({
1716
+ "mttr_hours": mttr,
1717
+ "within_days": within_days,
1718
+ "project_id": project_id,
1719
+ })
1720
+
1721
+ except Exception as e:
1722
+ return format_error("CALCULATE_MTTR_ERROR", str(e), retry=False)
1723
+
1724
+ return asyncio.run(_mttr())
1725
+
1726
+
1727
+ def incidents_sla_violations() -> str:
1728
+ """
1729
+ Find all incidents that violated SLA.
1730
+
1731
+ SLA: Critical incidents should be resolved within 4 hours.
1732
+
1733
+ Example:
1734
+ $ notion incidents sla-violations
1735
+ """
1736
+ async def _sla_violations() -> str:
1737
+ try:
1738
+ client = get_client()
1739
+
1740
+ # Register SDK plugin
1741
+ from better_notion.plugins.official.agents_sdk.plugin import AgentsSDKPlugin
1742
+ plugin = AgentsSDKPlugin()
1743
+ plugin.initialize(client)
1744
+ client.register_sdk_plugin(plugin)
1745
+
1746
+ # Get manager and find violations
1747
+ manager = client.plugin_manager("incidents")
1748
+ violations = await manager.find_sla_violations()
1749
+
1750
+ return format_success({
1751
+ "sla_violations": [
1752
+ {
1753
+ "id": incident.id,
1754
+ "title": incident.title,
1755
+ "severity": incident.severity,
1756
+ "discovery_date": str(incident.discovery_date) if incident.discovery_date else None,
1757
+ "resolved_date": str(incident.resolved_date) if incident.resolved_date else None,
1758
+ }
1759
+ for incident in violations
1760
+ ],
1761
+ "total": len(violations),
1762
+ })
1763
+
1764
+ except Exception as e:
1765
+ return format_error("SLA_VIOLATIONS_ERROR", str(e), retry=False)
1766
+
1767
+ return asyncio.run(_sla_violations())