jott-cli 0.7.1__tar.gz → 0.8.2__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.7.1/jott_cli.egg-info → jott_cli-0.8.2}/PKG-INFO +1 -1
  2. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/__init__.py +3 -13
  3. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/app.py +5 -0
  4. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_metadata_mixin.py +21 -10
  5. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_delete_mixin.py +51 -2
  6. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_metadata_mixin.py +15 -0
  7. jott_cli-0.8.2/jot/main.py +227 -0
  8. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/display_help.py +2 -1
  9. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/styles.py +43 -4
  10. {jott_cli-0.7.1 → jott_cli-0.8.2/jott_cli.egg-info}/PKG-INFO +1 -1
  11. {jott_cli-0.7.1 → jott_cli-0.8.2}/jott_cli.egg-info/SOURCES.txt +6 -0
  12. {jott_cli-0.7.1 → jott_cli-0.8.2}/pyproject.toml +1 -1
  13. jott_cli-0.8.2/tests/test_claude_org_prompt.py +90 -0
  14. jott_cli-0.8.2/tests/test_deleted_notes_dir.py +134 -0
  15. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_jot.py +1 -8
  16. jott_cli-0.8.2/tests/test_main_entry_point.py +84 -0
  17. jott_cli-0.8.2/tests/test_notes_tempfile_location.py +73 -0
  18. jott_cli-0.8.2/tests/test_state_palette.py +139 -0
  19. {jott_cli-0.7.1 → jott_cli-0.8.2}/LICENSE +0 -0
  20. {jott_cli-0.7.1 → jott_cli-0.8.2}/README.md +0 -0
  21. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/_app_navigation_mixin.py +0 -0
  22. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/_dispatch_mixin.py +0 -0
  23. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/categories/__init__.py +0 -0
  24. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/categories/config.py +0 -0
  25. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/categories/manager.py +0 -0
  26. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/categories/templates.py +0 -0
  27. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/cli/__init__.py +0 -0
  28. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/cli/archive.py +0 -0
  29. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/cli/config.py +0 -0
  30. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/cli/views.py +0 -0
  31. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/__init__.py +0 -0
  32. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_ai_analysis_mixin.py +0 -0
  33. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_ai_suggest_mixin.py +0 -0
  34. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_audio_timer_mixin.py +0 -0
  35. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_bulk_mixin.py +0 -0
  36. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_claude_mixin.py +0 -0
  37. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_context_mixin.py +0 -0
  38. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_core_mixin.py +0 -0
  39. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_gcal_mixin.py +0 -0
  40. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_github_mixin.py +0 -0
  41. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_notes_mixin.py +0 -0
  42. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_transfer_mixin.py +0 -0
  43. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/_web_clipboard_mixin.py +0 -0
  44. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/commands/handler.py +0 -0
  45. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/__init__.py +0 -0
  46. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_age_backlog_mixin.py +0 -0
  47. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_compress_mixin.py +0 -0
  48. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_crud_mixin.py +0 -0
  49. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_export_mixin.py +0 -0
  50. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_id_migration_mixin.py +0 -0
  51. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_navigation_mixin.py +0 -0
  52. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_persistence_mixin.py +0 -0
  53. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/_subtask_mixin.py +0 -0
  54. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/archive_manager.py +0 -0
  55. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/constants.py +0 -0
  56. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/id_manager.py +0 -0
  57. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/core/task_manager.py +0 -0
  58. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/__init__.py +0 -0
  59. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/gcal/__init__.py +0 -0
  60. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/gcal/account_manager.py +0 -0
  61. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/gcal/auth.py +0 -0
  62. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/gcal/events.py +0 -0
  63. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/github/__init__.py +0 -0
  64. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/github/issues.py +0 -0
  65. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/keywords/__init__.py +0 -0
  66. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/keywords/_config_mixin.py +0 -0
  67. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/keywords/_handlers_mixin.py +0 -0
  68. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/integrations/keywords/handler.py +0 -0
  69. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/mcp/__init__.py +0 -0
  70. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/mcp/handlers.py +0 -0
  71. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/mcp/schemas.py +0 -0
  72. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/mcp/server.py +0 -0
  73. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/projects/__init__.py +0 -0
  74. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/projects/backup.py +0 -0
  75. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/projects/registry.py +0 -0
  76. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/__init__.py +0 -0
  77. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/display.py +0 -0
  78. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/display_archive.py +0 -0
  79. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/display_footer.py +0 -0
  80. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/display_projects.py +0 -0
  81. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/display_tasks.py +0 -0
  82. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/formatting.py +0 -0
  83. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/input.py +0 -0
  84. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/picker.py +0 -0
  85. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/ui/rendering.py +0 -0
  86. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/utils/__init__.py +0 -0
  87. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/utils/date_utils.py +0 -0
  88. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/utils/text_utils.py +0 -0
  89. {jott_cli-0.7.1 → jott_cli-0.8.2}/jot/utils/validation.py +0 -0
  90. {jott_cli-0.7.1 → jott_cli-0.8.2}/jott_cli.egg-info/dependency_links.txt +0 -0
  91. {jott_cli-0.7.1 → jott_cli-0.8.2}/jott_cli.egg-info/entry_points.txt +0 -0
  92. {jott_cli-0.7.1 → jott_cli-0.8.2}/jott_cli.egg-info/requires.txt +0 -0
  93. {jott_cli-0.7.1 → jott_cli-0.8.2}/jott_cli.egg-info/top_level.txt +0 -0
  94. {jott_cli-0.7.1 → jott_cli-0.8.2}/setup.cfg +0 -0
  95. {jott_cli-0.7.1 → jott_cli-0.8.2}/setup.py +0 -0
  96. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_command_handler.py +0 -0
  97. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_dispatch.py +0 -0
  98. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_edit_edge_cases.py +0 -0
  99. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_fuzzy_search.py +0 -0
  100. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_gcal_notes.py +0 -0
  101. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_github.py +0 -0
  102. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_highlight.py +0 -0
  103. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_input.py +0 -0
  104. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_picker.py +0 -0
  105. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_styles.py +0 -0
  106. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_subtask_notes.py +0 -0
  107. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_terminal_wrap.py +0 -0
  108. {jott_cli-0.7.1 → jott_cli-0.8.2}/tests/test_today_filter.py +0 -0
  109. {jott_cli-0.7.1 → jott_cli-0.8.2}/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.7.1
3
+ Version: 0.8.2
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>
@@ -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.7.1"
8
+ __version__ = "0.8.2"
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',
@@ -84,6 +84,11 @@ class App(DispatchMixin, AppNavigationMixin):
84
84
  os.path.getmtime(task_manager.storage_file)
85
85
  if task_manager.storage_file.exists() else 0)
86
86
 
87
+ try:
88
+ task_manager._migrate_legacy_deleted_notes()
89
+ except Exception as e:
90
+ print(f"Warning: deleted-notes migration failed: {e}")
91
+
87
92
  # ------------------------------------------------------------------
88
93
  # Public entry point
89
94
  # ------------------------------------------------------------------
@@ -7,7 +7,7 @@ from jot.ui.picker import PickerItem, quick_picker, fuzzy_picker
7
7
  from jot.ui.styles import (
8
8
  RESET, BOLD, DIM, GREEN, RED,
9
9
  PRIORITY_COLORS, PRIORITY_SYMBOLS, STATUS_COLORS, STATUS_SYMBOLS,
10
- HIGHLIGHT_COLORS,
10
+ HIGHLIGHT_COLORS, STATE_ORDER, STATE_COLORS,
11
11
  )
12
12
  from jot.ui.input import restore_terminal
13
13
 
@@ -125,20 +125,29 @@ class MetadataMixin:
125
125
  return True
126
126
 
127
127
  def toggle_highlight(self):
128
- """Pick a highlight color for the current task, or clear it."""
128
+ """Pick a task state (color-coded) for the current task."""
129
129
  current_task = self.task_manager.get_current_task()
130
130
  if not current_task:
131
131
  return True
132
132
 
133
- items = [PickerItem(value="_clear", label="none (clear)", color=DIM)]
134
- for name, fmt in HIGHLIGHT_COLORS.items():
135
- items.append(PickerItem(
136
- value=name, label=name, color=fmt,
137
- ))
133
+ items = []
134
+ for state in STATE_ORDER:
135
+ color_key = STATE_COLORS[state]
136
+ if color_key is None:
137
+ items.append(PickerItem(
138
+ value="_clear", label="none", color=DIM,
139
+ annotation=state,
140
+ ))
141
+ else:
142
+ items.append(PickerItem(
143
+ value=color_key, label=color_key,
144
+ color=HIGHLIGHT_COLORS[color_key],
145
+ annotation=state,
146
+ ))
138
147
 
139
148
  result = quick_picker(
140
149
  items,
141
- prompt=f"Highlight color for: {current_task['text']}",
150
+ prompt=f"State for: {current_task['text']}",
142
151
  )
143
152
 
144
153
  if result.value is None:
@@ -146,11 +155,13 @@ class MetadataMixin:
146
155
 
147
156
  if result.value == "_clear":
148
157
  self.task_manager.set_highlight(color=None)
149
- print(f"\n{GREEN}✓{RESET} Highlight cleared")
158
+ print(f"\n{GREEN}✓{RESET} State cleared (backlog)")
150
159
  else:
151
160
  self.task_manager.set_highlight(color=result.value)
152
161
  fmt = HIGHLIGHT_COLORS[result.value]
153
- print(f"\n{GREEN}✓{RESET} Highlight set to {fmt} {result.value} {RESET}")
162
+ from jot.ui.styles import COLOR_TO_STATE
163
+ state_name = COLOR_TO_STATE.get(result.value, result.value)
164
+ print(f"\n{GREEN}✓{RESET} State: {fmt} {state_name} {RESET}")
154
165
 
155
166
  print("\nPress Enter to continue...", end='', flush=True)
156
167
  restore_terminal()
@@ -1,12 +1,55 @@
1
- """Delete mixin archive, permanent delete, notes export."""
1
+ """Delete mixin, archive, permanent delete, notes export."""
2
2
 
3
3
  import os
4
+ import re
4
5
  from datetime import datetime
5
6
 
6
7
 
8
+ DELETED_NOTES_DIR = ".jot-deleted-notes"
9
+ _DELETED_NOTES_FILENAME_RE = re.compile(
10
+ r"^TASK-\d+-notes-\d{8}_\d{6}\.org$"
11
+ )
12
+
13
+
7
14
  class DeleteMixin:
8
15
  """Task removal, archival, and notes preservation."""
9
16
 
17
+ def _migrate_legacy_deleted_notes(self):
18
+ """Move stray TASK-<id>-notes-<ts>.org files from project_dir
19
+ into the DELETED_NOTES_DIR subdirectory. Idempotent: a no-op
20
+ when no matching files exist at the project root.
21
+ """
22
+ try:
23
+ entries = list(self.project_dir.iterdir())
24
+ except (OSError, AttributeError):
25
+ return 0
26
+
27
+ matches = [
28
+ p for p in entries
29
+ if p.is_file() and _DELETED_NOTES_FILENAME_RE.match(p.name)
30
+ ]
31
+ if not matches:
32
+ return 0
33
+
34
+ dest_dir = self.project_dir / DELETED_NOTES_DIR
35
+ try:
36
+ dest_dir.mkdir(exist_ok=True)
37
+ except OSError as e:
38
+ print(f"Warning: could not create {dest_dir}: {e}")
39
+ return 0
40
+
41
+ moved = 0
42
+ for src in matches:
43
+ dest = dest_dir / src.name
44
+ if dest.exists():
45
+ continue
46
+ try:
47
+ src.rename(dest)
48
+ moved += 1
49
+ except OSError as e:
50
+ print(f"Warning: could not move {src.name}: {e}")
51
+ return moved
52
+
10
53
  def _unlink_children(self, task_id):
11
54
  """Remove parent:{task_id} labels from all children."""
12
55
  parent_label = f"parent:{task_id}"
@@ -131,7 +174,13 @@ class DeleteMixin:
131
174
  )
132
175
 
133
176
  filename = f"TASK-{task_id}-notes-{now.strftime('%Y%m%d_%H%M%S')}.org"
134
- org_file_path = self.project_dir / filename
177
+ dest_dir = self.project_dir / DELETED_NOTES_DIR
178
+ try:
179
+ dest_dir.mkdir(exist_ok=True)
180
+ except OSError as e:
181
+ print(f"Warning: Could not create {dest_dir}: {e}")
182
+ return None
183
+ org_file_path = dest_dir / filename
135
184
 
136
185
  try:
137
186
  with open(org_file_path, 'w') as f:
@@ -3,6 +3,18 @@
3
3
  from datetime import datetime, timezone
4
4
 
5
5
 
6
+ # One-way mapping from legacy status values to highlight color keys.
7
+ # Setting status auto-paints the highlight; the reverse is NOT applied.
8
+ # Status is being deprecated in favor of the richer state palette;
9
+ # this exists so the legacy `* status` picker still does something useful.
10
+ _STATUS_TO_HIGHLIGHT = {
11
+ 'todo': 'red',
12
+ 'in-progress': 'orange',
13
+ 'blocked': 'burnt',
14
+ 'done': 'yellow',
15
+ }
16
+
17
+
6
18
  class MetadataMixin:
7
19
  """Set task metadata: current flag, agent, day, priority, status, tally."""
8
20
 
@@ -118,6 +130,9 @@ class MetadataMixin:
118
130
  task['done'] = True
119
131
  elif task.get('done', False):
120
132
  task['done'] = False
133
+ color = _STATUS_TO_HIGHLIGHT.get(status)
134
+ if color:
135
+ task['highlight'] = color
121
136
  self._save_tasks()
122
137
  return True
123
138
  return False
@@ -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)
@@ -49,26 +49,65 @@ BLACK = '\033[30m'
49
49
  # Background colors
50
50
  BG_YELLOW = '\033[103m'
51
51
  BG_ORANGE = '\033[48;5;208m'
52
+ BG_BURNT_ORANGE = '\033[48;5;166m'
52
53
  BG_MAGENTA = '\033[105m'
53
54
  BG_GRAY = '\033[100m'
54
55
  BG_DARK_GRAY = '\033[48;5;236m'
55
56
  BG_CYAN = '\033[106m'
57
+ BG_BLUE = '\033[48;5;33m'
56
58
  BG_RED = '\033[101m'
57
59
  BG_GREEN = '\033[102m'
58
60
 
59
- # Semantic mappings highlight color palette
61
+ # Semantic mappings, highlight color palette.
62
+ # Each color maps to a task state in STATE_HIGHLIGHTS below; the legacy
63
+ # `status` field is being deprecated in favor of these state-bearing
64
+ # highlights.
60
65
  HIGHLIGHT_COLORS = {
61
66
  'gray': WHITE + BG_DARK_GRAY,
62
- 'yellow': BLACK + BG_YELLOW,
63
- 'orange': BLACK + BG_ORANGE,
64
67
  'magenta': WHITE + BG_MAGENTA,
65
68
  'cyan': BLACK + BG_CYAN,
66
69
  'red': WHITE + BG_RED,
70
+ 'orange': BLACK + BG_ORANGE,
71
+ 'burnt': WHITE + BG_BURNT_ORANGE,
72
+ 'yellow': BLACK + BG_YELLOW,
73
+ 'blue': WHITE + BG_BLUE,
67
74
  'green': BLACK + BG_GREEN,
68
75
  }
69
- HIGHLIGHT_DEFAULT = 'yellow'
76
+ HIGHLIGHT_DEFAULT = 'gray'
70
77
  HIGHLIGHT_FMT = HIGHLIGHT_COLORS[HIGHLIGHT_DEFAULT]
71
78
 
79
+ # Semantic mappings, task state pipeline.
80
+ # Ordered from earliest to latest; STATE_COLORS[state] is the
81
+ # highlight-color key (looks up into HIGHLIGHT_COLORS), or None for
82
+ # the unhighlighted "backlog" state.
83
+ STATE_ORDER = [
84
+ 'backlog',
85
+ 'needs-review',
86
+ 'in-review',
87
+ 'ready',
88
+ 'todo',
89
+ 'doing',
90
+ 'blocked',
91
+ 'done',
92
+ 'shipping',
93
+ 'complete',
94
+ ]
95
+ STATE_COLORS = {
96
+ 'backlog': None,
97
+ 'needs-review': 'gray',
98
+ 'in-review': 'magenta',
99
+ 'ready': 'cyan',
100
+ 'todo': 'red',
101
+ 'doing': 'orange',
102
+ 'blocked': 'burnt',
103
+ 'done': 'yellow',
104
+ 'shipping': 'blue',
105
+ 'complete': 'green',
106
+ }
107
+ # Reverse lookup: highlight-color key → state name (for one-way
108
+ # inference from the legacy color field if ever needed).
109
+ COLOR_TO_STATE = {v: k for k, v in STATE_COLORS.items() if v is not None}
110
+
72
111
  # Semantic mappings — priority
73
112
  PRIORITY_COLORS = {
74
113
  'none': DIM,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jott-cli
3
- Version: 0.7.1
3
+ Version: 0.8.2
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>
@@ -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
@@ -84,7 +85,9 @@ jott_cli.egg-info/dependency_links.txt
84
85
  jott_cli.egg-info/entry_points.txt
85
86
  jott_cli.egg-info/requires.txt
86
87
  jott_cli.egg-info/top_level.txt
88
+ tests/test_claude_org_prompt.py
87
89
  tests/test_command_handler.py
90
+ tests/test_deleted_notes_dir.py
88
91
  tests/test_dispatch.py
89
92
  tests/test_edit_edge_cases.py
90
93
  tests/test_fuzzy_search.py
@@ -93,7 +96,10 @@ tests/test_github.py
93
96
  tests/test_highlight.py
94
97
  tests/test_input.py
95
98
  tests/test_jot.py
99
+ tests/test_main_entry_point.py
100
+ tests/test_notes_tempfile_location.py
96
101
  tests/test_picker.py
102
+ tests/test_state_palette.py
97
103
  tests/test_styles.py
98
104
  tests/test_subtask_notes.py
99
105
  tests/test_terminal_wrap.py
@@ -7,7 +7,7 @@ include = ["jot", "jot.*"]
7
7
 
8
8
  [project]
9
9
  name = "jott-cli"
10
- version = "0.7.1"
10
+ version = "0.8.2"
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"
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for org-mode formatting instruction appended to Claude prompts."""
3
+
4
+ import tempfile
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+ from unittest.mock import patch
8
+
9
+ from jot import TaskManager
10
+ from jot.commands.handler import CommandHandler
11
+ from jot.commands._claude_mixin import _ORG_OUTPUT_INSTRUCTION
12
+
13
+
14
+ def _make_handler(tmpdir: Path):
15
+ (tmpdir / ".jot-registry.json").write_text("{}")
16
+ tm = TaskManager(storage_file='.jot.json', directory=str(tmpdir))
17
+ return tm, CommandHandler(tm)
18
+
19
+
20
+ def _fake_subprocess_result(stdout="* Result\nbody\n"):
21
+ return SimpleNamespace(stdout=stdout, stderr="", returncode=0)
22
+
23
+
24
+ class TestOrgOutputInstruction:
25
+ def test_instruction_constant_mentions_org_markers(self):
26
+ # Sanity: ensure the constant actually directs to org-mode output.
27
+ assert '#+begin_src' in _ORG_OUTPUT_INSTRUCTION
28
+ assert 'Do NOT use markdown' in _ORG_OUTPUT_INSTRUCTION
29
+ assert '~symbol~' in _ORG_OUTPUT_INSTRUCTION or '=literal=' in _ORG_OUTPUT_INSTRUCTION
30
+
31
+ def test_instruction_appended_when_no_existing_notes(self):
32
+ with tempfile.TemporaryDirectory() as raw:
33
+ tmpdir = Path(raw)
34
+ tm, handler = _make_handler(tmpdir)
35
+ tm.add_task("write a fizzbuzz")
36
+ task = tm.tasks[0]
37
+ with patch(
38
+ 'jot.commands._claude_mixin.subprocess.run',
39
+ return_value=_fake_subprocess_result(),
40
+ ) as mocked:
41
+ handler._run_claude_background(
42
+ task['id'], task['text'], existing_notes='',
43
+ allow_edits=False,
44
+ )
45
+
46
+ assert mocked.called
47
+ prompt = mocked.call_args.kwargs['input']
48
+ assert prompt.startswith("write a fizzbuzz")
49
+ assert prompt.endswith(_ORG_OUTPUT_INSTRUCTION)
50
+
51
+ def test_instruction_appended_when_existing_notes_present(self):
52
+ with tempfile.TemporaryDirectory() as raw:
53
+ tmpdir = Path(raw)
54
+ tm, handler = _make_handler(tmpdir)
55
+ tm.add_task("rewrite the parser")
56
+ task = tm.tasks[0]
57
+ with patch(
58
+ 'jot.commands._claude_mixin.subprocess.run',
59
+ return_value=_fake_subprocess_result(),
60
+ ) as mocked:
61
+ handler._run_claude_background(
62
+ task['id'], task['text'],
63
+ existing_notes='prior context here',
64
+ allow_edits=False,
65
+ )
66
+
67
+ prompt = mocked.call_args.kwargs['input']
68
+ assert 'rewrite the parser' in prompt
69
+ assert 'prior context here' in prompt
70
+ assert prompt.endswith(_ORG_OUTPUT_INSTRUCTION)
71
+
72
+ def test_instruction_appended_in_edit_mode(self):
73
+ # allow_edits=True path uses a different cmd but same prompt assembly.
74
+ with tempfile.TemporaryDirectory() as raw:
75
+ tmpdir = Path(raw)
76
+ tm, handler = _make_handler(tmpdir)
77
+ tm.add_task("apply diff")
78
+ task = tm.tasks[0]
79
+ with patch(
80
+ 'jot.commands._claude_mixin.subprocess.run',
81
+ return_value=_fake_subprocess_result(),
82
+ ) as mocked:
83
+ handler._run_claude_background(
84
+ task['id'], task['text'],
85
+ existing_notes='notes',
86
+ allow_edits=True,
87
+ )
88
+
89
+ prompt = mocked.call_args.kwargs['input']
90
+ assert prompt.endswith(_ORG_OUTPUT_INSTRUCTION)