jott-cli 0.8.1__tar.gz → 0.8.3__tar.gz

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 (109) hide show
  1. {jott_cli-0.8.1 → jott_cli-0.8.3}/PKG-INFO +11 -1
  2. {jott_cli-0.8.1 → jott_cli-0.8.3}/README.md +10 -0
  3. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/__init__.py +3 -13
  4. jott_cli-0.8.3/jot/main.py +227 -0
  5. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/display_help.py +2 -1
  6. {jott_cli-0.8.1 → jott_cli-0.8.3}/jott_cli.egg-info/PKG-INFO +11 -1
  7. {jott_cli-0.8.1 → jott_cli-0.8.3}/jott_cli.egg-info/SOURCES.txt +2 -0
  8. {jott_cli-0.8.1 → jott_cli-0.8.3}/pyproject.toml +1 -1
  9. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_jot.py +1 -8
  10. jott_cli-0.8.3/tests/test_main_entry_point.py +84 -0
  11. {jott_cli-0.8.1 → jott_cli-0.8.3}/LICENSE +0 -0
  12. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/_app_navigation_mixin.py +0 -0
  13. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/_dispatch_mixin.py +0 -0
  14. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/app.py +0 -0
  15. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/categories/__init__.py +0 -0
  16. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/categories/config.py +0 -0
  17. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/categories/manager.py +0 -0
  18. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/categories/templates.py +0 -0
  19. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/cli/__init__.py +0 -0
  20. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/cli/archive.py +0 -0
  21. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/cli/config.py +0 -0
  22. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/cli/views.py +0 -0
  23. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/__init__.py +0 -0
  24. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_ai_analysis_mixin.py +0 -0
  25. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_ai_suggest_mixin.py +0 -0
  26. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_audio_timer_mixin.py +0 -0
  27. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_bulk_mixin.py +0 -0
  28. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_claude_mixin.py +0 -0
  29. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_context_mixin.py +0 -0
  30. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_core_mixin.py +0 -0
  31. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_gcal_mixin.py +0 -0
  32. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_github_mixin.py +0 -0
  33. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_metadata_mixin.py +0 -0
  34. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_notes_mixin.py +0 -0
  35. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_transfer_mixin.py +0 -0
  36. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/_web_clipboard_mixin.py +0 -0
  37. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/commands/handler.py +0 -0
  38. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/__init__.py +0 -0
  39. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_age_backlog_mixin.py +0 -0
  40. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_compress_mixin.py +0 -0
  41. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_crud_mixin.py +0 -0
  42. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_delete_mixin.py +0 -0
  43. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_export_mixin.py +0 -0
  44. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_id_migration_mixin.py +0 -0
  45. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_metadata_mixin.py +0 -0
  46. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_navigation_mixin.py +0 -0
  47. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_persistence_mixin.py +0 -0
  48. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/_subtask_mixin.py +0 -0
  49. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/archive_manager.py +0 -0
  50. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/constants.py +0 -0
  51. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/id_manager.py +0 -0
  52. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/core/task_manager.py +0 -0
  53. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/__init__.py +0 -0
  54. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/gcal/__init__.py +0 -0
  55. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/gcal/account_manager.py +0 -0
  56. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/gcal/auth.py +0 -0
  57. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/gcal/events.py +0 -0
  58. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/github/__init__.py +0 -0
  59. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/github/issues.py +0 -0
  60. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/keywords/__init__.py +0 -0
  61. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/keywords/_config_mixin.py +0 -0
  62. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/keywords/_handlers_mixin.py +0 -0
  63. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/integrations/keywords/handler.py +0 -0
  64. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/mcp/__init__.py +0 -0
  65. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/mcp/handlers.py +0 -0
  66. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/mcp/schemas.py +0 -0
  67. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/mcp/server.py +0 -0
  68. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/projects/__init__.py +0 -0
  69. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/projects/backup.py +0 -0
  70. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/projects/registry.py +0 -0
  71. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/__init__.py +0 -0
  72. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/display.py +0 -0
  73. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/display_archive.py +0 -0
  74. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/display_footer.py +0 -0
  75. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/display_projects.py +0 -0
  76. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/display_tasks.py +0 -0
  77. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/formatting.py +0 -0
  78. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/input.py +0 -0
  79. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/picker.py +0 -0
  80. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/rendering.py +0 -0
  81. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/ui/styles.py +0 -0
  82. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/utils/__init__.py +0 -0
  83. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/utils/date_utils.py +0 -0
  84. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/utils/text_utils.py +0 -0
  85. {jott_cli-0.8.1 → jott_cli-0.8.3}/jot/utils/validation.py +0 -0
  86. {jott_cli-0.8.1 → jott_cli-0.8.3}/jott_cli.egg-info/dependency_links.txt +0 -0
  87. {jott_cli-0.8.1 → jott_cli-0.8.3}/jott_cli.egg-info/entry_points.txt +0 -0
  88. {jott_cli-0.8.1 → jott_cli-0.8.3}/jott_cli.egg-info/requires.txt +0 -0
  89. {jott_cli-0.8.1 → jott_cli-0.8.3}/jott_cli.egg-info/top_level.txt +0 -0
  90. {jott_cli-0.8.1 → jott_cli-0.8.3}/setup.cfg +0 -0
  91. {jott_cli-0.8.1 → jott_cli-0.8.3}/setup.py +0 -0
  92. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_claude_org_prompt.py +0 -0
  93. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_command_handler.py +0 -0
  94. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_deleted_notes_dir.py +0 -0
  95. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_dispatch.py +0 -0
  96. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_edit_edge_cases.py +0 -0
  97. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_fuzzy_search.py +0 -0
  98. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_gcal_notes.py +0 -0
  99. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_github.py +0 -0
  100. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_highlight.py +0 -0
  101. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_input.py +0 -0
  102. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_notes_tempfile_location.py +0 -0
  103. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_picker.py +0 -0
  104. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_state_palette.py +0 -0
  105. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_styles.py +0 -0
  106. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_subtask_notes.py +0 -0
  107. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_terminal_wrap.py +0 -0
  108. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_today_filter.py +0 -0
  109. {jott_cli-0.8.1 → jott_cli-0.8.3}/tests/test_transfer_subtasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jott-cli
3
- Version: 0.8.1
3
+ Version: 0.8.3
4
4
  Summary: Feature-rich interactive CLI task manager with AI integration, calendar sync, and keyword automation
5
5
  Author-email: Scott Anderson <sonander@gmail.com>
6
6
  Maintainer-email: Scott Anderson <sonander@gmail.com>
@@ -56,6 +56,16 @@ Dynamic: license-file
56
56
  [![Python 3.6+](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/)
57
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
58
 
59
+ ## TDD: Task-Driven Development
60
+
61
+ In this house, TDD is Task-Driven Development. The companion idea is JDD, Jott-Driven Design: design the thing by jotting it.
62
+
63
+ Jot the rough thought first, in whatever form works. A half-sentence, a verb, a "lol". That's the seed.
64
+
65
+ The seed grows. Notes attach to it. Subtasks split off. An analysis file or a Claude session forms around it. A status color shifts as it moves through review, dev, shipping. Eventually it ships, or it composts.
66
+
67
+ jott is the tool we use to do this. The methodology came from using it; the name came from the lol.
68
+
59
69
  ## Features
60
70
 
61
71
  - **Quick-add by default**: Just start typing, press Enter to save
@@ -6,6 +6,16 @@
6
6
  [![Python 3.6+](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
+ ## TDD: Task-Driven Development
10
+
11
+ In this house, TDD is Task-Driven Development. The companion idea is JDD, Jott-Driven Design: design the thing by jotting it.
12
+
13
+ Jot the rough thought first, in whatever form works. A half-sentence, a verb, a "lol". That's the seed.
14
+
15
+ The seed grows. Notes attach to it. Subtasks split off. An analysis file or a Claude session forms around it. A status color shifts as it moves through review, dev, shipping. Eventually it ships, or it composts.
16
+
17
+ jott is the tool we use to do this. The methodology came from using it; the name came from the lol.
18
+
9
19
  ## Features
10
20
 
11
21
  - **Quick-add by default**: Just start typing, press Enter to save
@@ -3,18 +3,15 @@
3
3
  This package provides modular components for the jott task management system.
4
4
  """
5
5
 
6
- import importlib.util
7
6
  import os
8
- from pathlib import Path
9
7
 
10
- __version__ = "0.8.1"
8
+ __version__ = "0.8.3"
11
9
 
12
- # When building Sphinx docs, skip the dynamic import bridge and heavy deps.
10
+ # When building Sphinx docs, skip the heavy runtime imports.
13
11
  # Set JOT_SPHINX_BUILD=1 in docs/sphinx/conf.py before importing jot.
14
12
  if os.environ.get('JOT_SPHINX_BUILD'):
15
13
  main = None
16
14
  else:
17
- # Import extracted classes from their new modules
18
15
  from jot.core.id_manager import IDManager
19
16
  from jot.core.task_manager import TaskManager
20
17
  from jot.projects.registry import ProjectRegistry
@@ -27,14 +24,7 @@ else:
27
24
  from jot.integrations.gcal.events import create_gcal_event, fetch_gcal_events
28
25
  from jot.integrations.keywords.handler import KeywordHandler
29
26
 
30
- # Load jot.py for main() and other not-yet-extracted components
31
- _jot_script_path = Path(__file__).parent.parent / 'jot.py'
32
- _spec = importlib.util.spec_from_file_location("_jot_script", _jot_script_path)
33
- _jot_script = importlib.util.module_from_spec(_spec)
34
- _spec.loader.exec_module(_jot_script)
35
-
36
- # Export main function from jot.py
37
- main = _jot_script.main
27
+ from jot.main import main
38
28
 
39
29
  __all__ = [
40
30
  'main',
@@ -0,0 +1,227 @@
1
+ """jott entry point.
2
+
3
+ Hosts main() and the CLI argument plumbing previously in the
4
+ repo-root jot.py script. The console-script entry point in
5
+ pyproject.toml resolves through jot.__init__, which re-exports
6
+ main from this module.
7
+ """
8
+
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from jot.core.task_manager import TaskManager
14
+ from jot.projects.registry import ProjectRegistry
15
+ from jot.categories.manager import CategoryManager
16
+ from jot.commands import CommandHandler
17
+ from jot.cli import handle_cli_args
18
+ from jot.ui import display_help
19
+
20
+
21
+ def load_auto_backlog_config():
22
+ """Load auto-backlog configuration from ~/.jot-config.json"""
23
+ config_file = Path.home() / '.jot-config.json'
24
+ default_config = {
25
+ 'enabled': True,
26
+ 'age_threshold_days': 30,
27
+ 'backlog_category': 'backlog',
28
+ }
29
+
30
+ if not config_file.exists():
31
+ return default_config
32
+
33
+ try:
34
+ with open(config_file, 'r') as f:
35
+ config = json.load(f)
36
+ result = default_config.copy()
37
+ result.update(config.get('auto_backlog', {}))
38
+ return result
39
+ except (json.JSONDecodeError, IOError):
40
+ return default_config
41
+
42
+
43
+ def _parse_flags(args):
44
+ """Extract --all, --global, --category/-c flags from args.
45
+
46
+ Returns (args, broadcast_all, explicit_global, category) with
47
+ consumed flags removed from args.
48
+ """
49
+ broadcast_all = '--all' in args
50
+ if broadcast_all:
51
+ args.remove('--all')
52
+
53
+ explicit_global = '--global' in args
54
+ if explicit_global:
55
+ args.remove('--global')
56
+
57
+ category = None
58
+ for flag in ('--category', '-c'):
59
+ if flag in args:
60
+ idx = args.index(flag)
61
+ if idx + 1 < len(args):
62
+ category = args[idx + 1]
63
+ args = args[:idx] + args[idx + 2 :]
64
+ break
65
+
66
+ return args, broadcast_all, explicit_global, category
67
+
68
+
69
+ def _resolve_global(category, explicit_global, target_dir):
70
+ """Determine whether a category should be global or local."""
71
+ if not category:
72
+ return False
73
+
74
+ if explicit_global:
75
+ return True
76
+
77
+ cat_manager = CategoryManager(project_dir=target_dir or Path.cwd())
78
+ location = cat_manager.resolve_category_location(category)
79
+
80
+ if location == 'global':
81
+ return True
82
+ if location == 'local':
83
+ return False
84
+
85
+ # New category — create locally by default
86
+ can_create, error_msg = cat_manager.can_create_category(category)
87
+ if not can_create:
88
+ print(f"✗ {error_msg}")
89
+ sys.exit(1)
90
+
91
+ warning_msg = cat_manager.get_warning_message(category)
92
+ if warning_msg:
93
+ print(warning_msg)
94
+
95
+ return False
96
+
97
+
98
+ def _resolve_project_and_task(args, registry):
99
+ """Parse remaining args into (target_dir, project_name, initial_task)."""
100
+ target_dir = None
101
+ project_name = None
102
+ initial_task = None
103
+
104
+ if args:
105
+ project_path = registry.get_project_path(args[0])
106
+ if project_path:
107
+ target_dir = project_path
108
+ project_name = args[0]
109
+ if len(args) > 1:
110
+ initial_task = ' '.join(args[1:])
111
+ else:
112
+ initial_task = ' '.join(args)
113
+
114
+ return target_dir, project_name, initial_task
115
+
116
+
117
+ def _handle_initial_task(
118
+ task_manager, initial_task, broadcast_all, registry, project_name, is_global, category
119
+ ):
120
+ """Add a task supplied on the command line and exit."""
121
+ if initial_task and broadcast_all:
122
+ projects = registry.list_projects()
123
+ if not projects:
124
+ print("✗ No registered projects found. Use --register to add projects.")
125
+ return
126
+
127
+ n = len(projects)
128
+ confirm = input(f"⚠ Broadcast \"{initial_task}\" to {n} projects? (y/N): ").strip().lower()
129
+ if confirm != 'y':
130
+ print("Cancelled.")
131
+ return
132
+
133
+ added = 0
134
+ seen_paths = set()
135
+ for _, proj_path in projects.items():
136
+ resolved = str(Path(proj_path).resolve())
137
+ if resolved in seen_paths:
138
+ continue
139
+ seen_paths.add(resolved)
140
+ tm = TaskManager(directory=proj_path, project_registry=registry)
141
+ all_texts = [t['text'] for t in tm.tasks + tm.archived]
142
+ if initial_task in all_texts:
143
+ continue
144
+ tm.add_task(initial_task)
145
+ added += 1
146
+ print(f"✓ Added task to {added}/{len(projects)} projects: {initial_task}")
147
+ return
148
+
149
+ if initial_task:
150
+ task_manager.add_task(initial_task)
151
+ location = project_name or Path.cwd().name
152
+
153
+ if is_global and category:
154
+ print(f"✓ Added task to global ({category}): {initial_task}")
155
+ elif category:
156
+ print(f"✓ Added task to {location} ({category}): {initial_task}")
157
+ else:
158
+ print(f"✓ Added task to {location}: {initial_task}")
159
+
160
+
161
+ def main():
162
+ """Entry point: parse flags, dispatch CLI commands, or start TUI."""
163
+ registry = ProjectRegistry()
164
+ args = sys.argv[1:]
165
+
166
+ # --help is always checked first
167
+ if '--help' in args or '-h' in args:
168
+ display_help()
169
+ return
170
+
171
+ args, broadcast_all, explicit_global, category = _parse_flags(args)
172
+
173
+ # One-shot CLI commands (--list-projects, --archive, --today, etc.)
174
+ if handle_cli_args(args, registry, category, explicit_global):
175
+ return
176
+
177
+ # Resolve project routing and initial task text
178
+ target_dir, project_name, initial_task = _resolve_project_and_task(args, registry)
179
+
180
+ is_global = _resolve_global(category, explicit_global, target_dir)
181
+
182
+ # Set up TaskManager and CommandHandler
183
+ task_manager = TaskManager(
184
+ directory=target_dir,
185
+ category=category,
186
+ is_global=is_global,
187
+ project_registry=registry,
188
+ )
189
+ command_handler = CommandHandler(task_manager, registry)
190
+
191
+ # Auto-register project if opened with .jot.json but not in registry
192
+ proj_dir = task_manager.project_dir
193
+ if (
194
+ not is_global
195
+ and (proj_dir / '.jot.json').exists()
196
+ and not registry.is_path_registered(proj_dir)
197
+ ):
198
+ registry.register_project(proj_dir.name, str(proj_dir))
199
+
200
+ # Auto-transfer stale tasks to backlog
201
+ auto_cfg = load_auto_backlog_config()
202
+ if auto_cfg.get('enabled', True) and category != auto_cfg.get('backlog_category', 'backlog'):
203
+ task_manager.transfer_old_tasks_to_backlog(
204
+ age_threshold_days=auto_cfg.get('age_threshold_days', 30),
205
+ backlog_category=auto_cfg.get('backlog_category', 'backlog'),
206
+ )
207
+
208
+ task_manager.ensure_default_task()
209
+
210
+ # If a task was supplied on the command line, add it and exit
211
+ if initial_task or broadcast_all:
212
+ _handle_initial_task(
213
+ task_manager,
214
+ initial_task,
215
+ broadcast_all,
216
+ registry,
217
+ project_name,
218
+ is_global,
219
+ category,
220
+ )
221
+ return
222
+
223
+ # Launch the interactive TUI
224
+ from jot.app import App
225
+
226
+ app = App(task_manager, command_handler, registry, target_dir, project_name)
227
+ app.run()
@@ -257,9 +257,10 @@ def display_categorized_shortcuts():
257
257
 
258
258
  def display_help():
259
259
  """Display comprehensive help information."""
260
+ from jot import __version__
260
261
  help_text = f"""
261
262
  {BOLD}Jott - Simple Interactive Task List{RESET}
262
- {DIM}Version 0.7.1{RESET}
263
+ {DIM}Version {__version__}{RESET}
263
264
 
264
265
  {BOLD}BASIC USAGE:{RESET}
265
266
  jott Start interactive task manager (current project)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jott-cli
3
- Version: 0.8.1
3
+ Version: 0.8.3
4
4
  Summary: Feature-rich interactive CLI task manager with AI integration, calendar sync, and keyword automation
5
5
  Author-email: Scott Anderson <sonander@gmail.com>
6
6
  Maintainer-email: Scott Anderson <sonander@gmail.com>
@@ -56,6 +56,16 @@ Dynamic: license-file
56
56
  [![Python 3.6+](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/)
57
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
58
 
59
+ ## TDD: Task-Driven Development
60
+
61
+ In this house, TDD is Task-Driven Development. The companion idea is JDD, Jott-Driven Design: design the thing by jotting it.
62
+
63
+ Jot the rough thought first, in whatever form works. A half-sentence, a verb, a "lol". That's the seed.
64
+
65
+ The seed grows. Notes attach to it. Subtasks split off. An analysis file or a Claude session forms around it. A status color shifts as it moves through review, dev, shipping. Eventually it ships, or it composts.
66
+
67
+ jott is the tool we use to do this. The methodology came from using it; the name came from the lol.
68
+
59
69
  ## Features
60
70
 
61
71
  - **Quick-add by default**: Just start typing, press Enter to save
@@ -6,6 +6,7 @@ jot/__init__.py
6
6
  jot/_app_navigation_mixin.py
7
7
  jot/_dispatch_mixin.py
8
8
  jot/app.py
9
+ jot/main.py
9
10
  jot/categories/__init__.py
10
11
  jot/categories/config.py
11
12
  jot/categories/manager.py
@@ -95,6 +96,7 @@ tests/test_github.py
95
96
  tests/test_highlight.py
96
97
  tests/test_input.py
97
98
  tests/test_jot.py
99
+ tests/test_main_entry_point.py
98
100
  tests/test_notes_tempfile_location.py
99
101
  tests/test_picker.py
100
102
  tests/test_state_palette.py
@@ -7,7 +7,7 @@ include = ["jot", "jot.*"]
7
7
 
8
8
  [project]
9
9
  name = "jott-cli"
10
- version = "0.8.1"
10
+ version = "0.8.3"
11
11
  description = "Feature-rich interactive CLI task manager with AI integration, calendar sync, and keyword automation"
12
12
  readme = {file = "README.md", content-type = "text/markdown"}
13
13
  requires-python = ">=3.6"
@@ -3390,14 +3390,7 @@ class TestImportIntegrity:
3390
3390
  def test_cli_broadcast_requires_confirmation(self):
3391
3391
  """CLI --all broadcast requires y confirmation, N cancels"""
3392
3392
  from unittest.mock import patch
3393
- import importlib.util
3394
-
3395
- spec = importlib.util.spec_from_file_location(
3396
- "jot_main", str(Path(__file__).parent.parent / "jot.py")
3397
- )
3398
- jot_main = importlib.util.module_from_spec(spec)
3399
- spec.loader.exec_module(jot_main)
3400
- _handle_initial_task = jot_main._handle_initial_task
3393
+ from jot.main import _handle_initial_task
3401
3394
 
3402
3395
  with tempfile.TemporaryDirectory() as tmpdir:
3403
3396
  tmpdir = Path(tmpdir)
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for the package entry point.
3
+
4
+ Earlier versions used a dynamic-import bridge that loaded main() from
5
+ the repo-root jot.py script via spec_from_file_location. That worked
6
+ locally (where jot.py sat next to the jot/ package) but failed under
7
+ pip install because jot.py was not part of the wheel:
8
+
9
+ FileNotFoundError: '.../site-packages/jot.py'
10
+
11
+ The fix moved main() into jot/main.py so the console script resolves
12
+ through a real package submodule.
13
+ """
14
+
15
+ import importlib
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+
21
+ class TestMainEntryPoint:
22
+ def test_main_is_a_real_package_submodule(self):
23
+ """jot.main must exist as a real importable submodule, not a
24
+ path-loaded shim. Importing it must not depend on any file
25
+ outside site-packages.
26
+ """
27
+ import importlib
28
+ main_module = importlib.import_module('jot.main')
29
+ main_path = Path(main_module.__file__).resolve()
30
+ assert main_path.name == 'main.py'
31
+ assert main_path.parent.name == 'jot', (
32
+ f"jot.main should live inside the jot/ package, "
33
+ f"got {main_path}"
34
+ )
35
+
36
+ def test_main_is_exported_from_package(self):
37
+ """`from jot import main` must yield a callable."""
38
+ # Reload to ensure we exercise the real import path.
39
+ if 'jot' in sys.modules:
40
+ importlib.reload(sys.modules['jot'])
41
+ from jot import main
42
+ assert callable(main)
43
+
44
+ def test_init_does_not_use_path_based_loader(self):
45
+ """Guard against re-introduction of the dynamic bridge."""
46
+ import jot
47
+ init_src = Path(jot.__file__).read_text()
48
+ assert 'spec_from_file_location' not in init_src, (
49
+ "jot/__init__.py must not load main from a file path; "
50
+ "it should import jot.main as a normal submodule"
51
+ )
52
+
53
+ def test_console_entry_point_resolves(self):
54
+ """Simulate what `jott` does: `import jot; jot.main` is the
55
+ target referenced by pyproject.toml's console_scripts entry.
56
+ """
57
+ import jot
58
+ assert hasattr(jot, 'main')
59
+ assert callable(jot.main)
60
+
61
+
62
+ class TestInstalledLayout:
63
+ """Verify the layout that pip install produces. Skips silently if
64
+ we can't find a wheel to inspect (e.g., fresh checkout pre-build).
65
+ """
66
+
67
+ def test_built_wheel_contains_main_module(self):
68
+ dist = Path(__file__).resolve().parents[1] / 'dist'
69
+ if not dist.exists():
70
+ return # no build artifacts; skip
71
+ wheels = sorted(dist.glob('jott_cli-*.whl'))
72
+ if not wheels:
73
+ return
74
+ latest = wheels[-1]
75
+
76
+ result = subprocess.run(
77
+ ['unzip', '-l', str(latest)],
78
+ capture_output=True, text=True,
79
+ )
80
+ listing = result.stdout
81
+ assert 'jot/main.py' in listing, (
82
+ f"wheel {latest.name} is missing jot/main.py; the entry "
83
+ f"point will fail on install. Listing:\n{listing}"
84
+ )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes