codealmanac 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. codealmanac/__init__.py +13 -0
  2. codealmanac/app.py +175 -0
  3. codealmanac/cli/__init__.py +1 -0
  4. codealmanac/cli/dispatch/__init__.py +0 -0
  5. codealmanac/cli/dispatch/admin.py +124 -0
  6. codealmanac/cli/dispatch/config.py +50 -0
  7. codealmanac/cli/dispatch/root.py +328 -0
  8. codealmanac/cli/main.py +28 -0
  9. codealmanac/cli/parser/__init__.py +0 -0
  10. codealmanac/cli/parser/admin.py +81 -0
  11. codealmanac/cli/parser/lifecycle.py +57 -0
  12. codealmanac/cli/parser/root.py +19 -0
  13. codealmanac/cli/parser/wiki.py +87 -0
  14. codealmanac/cli/render/__init__.py +0 -0
  15. codealmanac/cli/render/admin.py +191 -0
  16. codealmanac/cli/render/root.py +290 -0
  17. codealmanac/core/__init__.py +1 -0
  18. codealmanac/core/errors.py +45 -0
  19. codealmanac/core/models.py +14 -0
  20. codealmanac/core/paths.py +25 -0
  21. codealmanac/core/slug.py +7 -0
  22. codealmanac/core/text.py +5 -0
  23. codealmanac/database/__init__.py +15 -0
  24. codealmanac/database/sqlite.py +54 -0
  25. codealmanac/integrations/__init__.py +1 -0
  26. codealmanac/integrations/automation/__init__.py +3 -0
  27. codealmanac/integrations/automation/scheduler/__init__.py +5 -0
  28. codealmanac/integrations/automation/scheduler/launchd.py +163 -0
  29. codealmanac/integrations/command.py +56 -0
  30. codealmanac/integrations/harnesses/__init__.py +7 -0
  31. codealmanac/integrations/harnesses/claude/__init__.py +1 -0
  32. codealmanac/integrations/harnesses/claude/adapter.py +217 -0
  33. codealmanac/integrations/harnesses/codex/__init__.py +3 -0
  34. codealmanac/integrations/harnesses/codex/adapter.py +221 -0
  35. codealmanac/integrations/harnesses/git_status.py +49 -0
  36. codealmanac/integrations/sources/__init__.py +29 -0
  37. codealmanac/integrations/sources/filesystem/__init__.py +5 -0
  38. codealmanac/integrations/sources/filesystem/adapter.py +685 -0
  39. codealmanac/integrations/sources/filesystem/selection.py +209 -0
  40. codealmanac/integrations/sources/git/__init__.py +3 -0
  41. codealmanac/integrations/sources/git/adapter.py +132 -0
  42. codealmanac/integrations/sources/github/__init__.py +3 -0
  43. codealmanac/integrations/sources/github/adapter.py +413 -0
  44. codealmanac/integrations/sources/runtime.py +22 -0
  45. codealmanac/integrations/sources/transcripts/__init__.py +33 -0
  46. codealmanac/integrations/sources/transcripts/claude.py +61 -0
  47. codealmanac/integrations/sources/transcripts/codex.py +69 -0
  48. codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
  49. codealmanac/integrations/sources/transcripts/runtime.py +387 -0
  50. codealmanac/integrations/sources/web/__init__.py +3 -0
  51. codealmanac/integrations/sources/web/adapter.py +303 -0
  52. codealmanac/integrations/updates/__init__.py +7 -0
  53. codealmanac/integrations/updates/package.py +85 -0
  54. codealmanac/integrations/workspaces/__init__.py +1 -0
  55. codealmanac/integrations/workspaces/git/__init__.py +3 -0
  56. codealmanac/integrations/workspaces/git/probe.py +128 -0
  57. codealmanac/manual/README.md +24 -0
  58. codealmanac/manual/__init__.py +19 -0
  59. codealmanac/manual/build.md +20 -0
  60. codealmanac/manual/evidence.md +23 -0
  61. codealmanac/manual/garden.md +20 -0
  62. codealmanac/manual/ingest.md +17 -0
  63. codealmanac/manual/library.py +84 -0
  64. codealmanac/manual/models.py +83 -0
  65. codealmanac/manual/pages.md +28 -0
  66. codealmanac/manual/requests.py +6 -0
  67. codealmanac/manual/sources.md +18 -0
  68. codealmanac/manual/style.md +19 -0
  69. codealmanac/prompts/__init__.py +5 -0
  70. codealmanac/prompts/base/notability.md +14 -0
  71. codealmanac/prompts/base/purpose.md +23 -0
  72. codealmanac/prompts/base/syntax.md +19 -0
  73. codealmanac/prompts/models.py +9 -0
  74. codealmanac/prompts/operations/garden.md +26 -0
  75. codealmanac/prompts/operations/ingest.md +18 -0
  76. codealmanac/prompts/renderer.py +24 -0
  77. codealmanac/prompts/requests.py +22 -0
  78. codealmanac/server/__init__.py +1 -0
  79. codealmanac/server/app.py +202 -0
  80. codealmanac/server/assets/__init__.py +1 -0
  81. codealmanac/server/assets/app.css +865 -0
  82. codealmanac/server/assets/app.js +3 -0
  83. codealmanac/server/assets/index.html +80 -0
  84. codealmanac/server/assets/viewer/api.js +30 -0
  85. codealmanac/server/assets/viewer/components.js +197 -0
  86. codealmanac/server/assets/viewer/main.js +126 -0
  87. codealmanac/server/assets/viewer/renderers.js +122 -0
  88. codealmanac/server/assets/viewer/routes.js +36 -0
  89. codealmanac/services/__init__.py +1 -0
  90. codealmanac/services/automation/__init__.py +3 -0
  91. codealmanac/services/automation/models.py +83 -0
  92. codealmanac/services/automation/ports.py +14 -0
  93. codealmanac/services/automation/requests.py +40 -0
  94. codealmanac/services/automation/service.py +294 -0
  95. codealmanac/services/config/__init__.py +17 -0
  96. codealmanac/services/config/models.py +61 -0
  97. codealmanac/services/config/requests.py +21 -0
  98. codealmanac/services/config/service.py +55 -0
  99. codealmanac/services/config/store.py +26 -0
  100. codealmanac/services/diagnostics/__init__.py +1 -0
  101. codealmanac/services/diagnostics/models.py +22 -0
  102. codealmanac/services/diagnostics/requests.py +8 -0
  103. codealmanac/services/diagnostics/service.py +283 -0
  104. codealmanac/services/harnesses/__init__.py +1 -0
  105. codealmanac/services/harnesses/models.py +104 -0
  106. codealmanac/services/harnesses/ports.py +18 -0
  107. codealmanac/services/harnesses/requests.py +19 -0
  108. codealmanac/services/harnesses/service.py +38 -0
  109. codealmanac/services/health/__init__.py +1 -0
  110. codealmanac/services/health/requests.py +8 -0
  111. codealmanac/services/health/service.py +20 -0
  112. codealmanac/services/index/__init__.py +1 -0
  113. codealmanac/services/index/models.py +135 -0
  114. codealmanac/services/index/requests.py +26 -0
  115. codealmanac/services/index/service.py +86 -0
  116. codealmanac/services/index/store.py +411 -0
  117. codealmanac/services/index/views.py +524 -0
  118. codealmanac/services/pages/__init__.py +1 -0
  119. codealmanac/services/pages/requests.py +17 -0
  120. codealmanac/services/pages/service.py +26 -0
  121. codealmanac/services/runs/__init__.py +1 -0
  122. codealmanac/services/runs/models.py +91 -0
  123. codealmanac/services/runs/requests.py +76 -0
  124. codealmanac/services/runs/service.py +86 -0
  125. codealmanac/services/runs/store.py +256 -0
  126. codealmanac/services/search/__init__.py +1 -0
  127. codealmanac/services/search/requests.py +23 -0
  128. codealmanac/services/search/service.py +31 -0
  129. codealmanac/services/sources/__init__.py +1 -0
  130. codealmanac/services/sources/models.py +126 -0
  131. codealmanac/services/sources/ports.py +30 -0
  132. codealmanac/services/sources/requests.py +76 -0
  133. codealmanac/services/sources/service.py +351 -0
  134. codealmanac/services/tagging/__init__.py +1 -0
  135. codealmanac/services/tagging/models.py +9 -0
  136. codealmanac/services/tagging/requests.py +35 -0
  137. codealmanac/services/tagging/service.py +43 -0
  138. codealmanac/services/topics/__init__.py +1 -0
  139. codealmanac/services/topics/models.py +36 -0
  140. codealmanac/services/topics/requests.py +115 -0
  141. codealmanac/services/topics/service.py +297 -0
  142. codealmanac/services/updates/__init__.py +4 -0
  143. codealmanac/services/updates/models.py +83 -0
  144. codealmanac/services/updates/ports.py +17 -0
  145. codealmanac/services/updates/requests.py +10 -0
  146. codealmanac/services/updates/service.py +113 -0
  147. codealmanac/services/viewer/__init__.py +1 -0
  148. codealmanac/services/viewer/models.py +80 -0
  149. codealmanac/services/viewer/renderer.py +89 -0
  150. codealmanac/services/viewer/requests.py +86 -0
  151. codealmanac/services/viewer/service.py +211 -0
  152. codealmanac/services/wiki/__init__.py +1 -0
  153. codealmanac/services/wiki/documents.py +83 -0
  154. codealmanac/services/wiki/frontmatter.py +94 -0
  155. codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
  156. codealmanac/services/wiki/models.py +69 -0
  157. codealmanac/services/wiki/paths.py +42 -0
  158. codealmanac/services/wiki/service.py +57 -0
  159. codealmanac/services/wiki/templates.py +73 -0
  160. codealmanac/services/wiki/topics.py +266 -0
  161. codealmanac/services/wiki/wikilinks.py +58 -0
  162. codealmanac/services/workspaces/__init__.py +1 -0
  163. codealmanac/services/workspaces/models.py +124 -0
  164. codealmanac/services/workspaces/ports.py +9 -0
  165. codealmanac/services/workspaces/requests.py +82 -0
  166. codealmanac/services/workspaces/roots.py +74 -0
  167. codealmanac/services/workspaces/service.py +303 -0
  168. codealmanac/services/workspaces/store.py +127 -0
  169. codealmanac/workflows/__init__.py +1 -0
  170. codealmanac/workflows/build/__init__.py +1 -0
  171. codealmanac/workflows/build/models.py +8 -0
  172. codealmanac/workflows/build/service.py +45 -0
  173. codealmanac/workflows/garden/__init__.py +3 -0
  174. codealmanac/workflows/garden/models.py +30 -0
  175. codealmanac/workflows/garden/requests.py +22 -0
  176. codealmanac/workflows/garden/service.py +239 -0
  177. codealmanac/workflows/ingest/__init__.py +1 -0
  178. codealmanac/workflows/ingest/models.py +26 -0
  179. codealmanac/workflows/ingest/requests.py +39 -0
  180. codealmanac/workflows/ingest/service.py +302 -0
  181. codealmanac/workflows/lifecycle.py +197 -0
  182. codealmanac/workflows/sync/__init__.py +3 -0
  183. codealmanac/workflows/sync/models.py +157 -0
  184. codealmanac/workflows/sync/requests.py +63 -0
  185. codealmanac/workflows/sync/service.py +651 -0
  186. codealmanac/workflows/sync/store.py +51 -0
  187. codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
  188. codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
  189. codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
  190. codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  191. codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
  192. codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ __all__ = ["__version__"]
4
+
5
+
6
+ def _installed_version() -> str:
7
+ try:
8
+ return version("codealmanac")
9
+ except PackageNotFoundError:
10
+ return "0+unknown"
11
+
12
+
13
+ __version__ = _installed_version()
codealmanac/app.py ADDED
@@ -0,0 +1,175 @@
1
+ from collections.abc import Sequence
2
+ from dataclasses import dataclass
3
+
4
+ from codealmanac import __version__
5
+ from codealmanac.core.models import AppConfig
6
+ from codealmanac.integrations.automation import LaunchdSchedulerAdapter
7
+ from codealmanac.integrations.harnesses import default_harness_adapters
8
+ from codealmanac.integrations.sources import (
9
+ default_source_runtime_adapters,
10
+ default_transcript_discovery_adapters,
11
+ )
12
+ from codealmanac.integrations.updates import (
13
+ InstalledPackageMetadataProvider,
14
+ SubprocessPackageCommandRunner,
15
+ )
16
+ from codealmanac.integrations.workspaces.git import GitWorkspaceChangeProbe
17
+ from codealmanac.manual import ManualLibrary
18
+ from codealmanac.prompts import PromptRenderer
19
+ from codealmanac.services.automation.ports import SchedulerAdapter
20
+ from codealmanac.services.automation.service import AutomationService
21
+ from codealmanac.services.config.service import ConfigService
22
+ from codealmanac.services.config.store import ConfigStore
23
+ from codealmanac.services.diagnostics.service import DiagnosticsService
24
+ from codealmanac.services.harnesses.ports import HarnessAdapter
25
+ from codealmanac.services.harnesses.service import HarnessesService
26
+ from codealmanac.services.health.service import HealthService
27
+ from codealmanac.services.index.service import IndexService
28
+ from codealmanac.services.index.store import IndexStore
29
+ from codealmanac.services.pages.service import PagesService
30
+ from codealmanac.services.runs.service import RunsService
31
+ from codealmanac.services.runs.store import RunStore
32
+ from codealmanac.services.search.service import SearchService
33
+ from codealmanac.services.sources.ports import (
34
+ SourceRuntimeAdapter,
35
+ TranscriptDiscoveryAdapter,
36
+ )
37
+ from codealmanac.services.sources.service import SourcesService
38
+ from codealmanac.services.tagging.service import TaggingService
39
+ from codealmanac.services.topics.service import TopicsService
40
+ from codealmanac.services.updates.ports import (
41
+ PackageCommandRunner,
42
+ PackageInstallMetadataProvider,
43
+ )
44
+ from codealmanac.services.updates.service import UpdatesService
45
+ from codealmanac.services.viewer.renderer import MarkdownRenderer
46
+ from codealmanac.services.viewer.service import ViewerService
47
+ from codealmanac.services.wiki.service import WikiService
48
+ from codealmanac.services.workspaces.service import WorkspacesService
49
+ from codealmanac.services.workspaces.store import WorkspaceRegistryStore
50
+ from codealmanac.workflows.build.service import BuildWorkflow
51
+ from codealmanac.workflows.garden.service import GardenWorkflow
52
+ from codealmanac.workflows.ingest.service import IngestWorkflow
53
+ from codealmanac.workflows.lifecycle import LifecycleMutationPolicy
54
+ from codealmanac.workflows.sync.service import SyncWorkflow
55
+ from codealmanac.workflows.sync.store import SyncLedgerStore
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class CodeAlmanacWorkflows:
60
+ build: BuildWorkflow
61
+ ingest: IngestWorkflow
62
+ garden: GardenWorkflow
63
+ sync: SyncWorkflow
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class CodeAlmanac:
68
+ automation: AutomationService
69
+ config: ConfigService
70
+ workspaces: WorkspacesService
71
+ wiki: WikiService
72
+ index: IndexService
73
+ search: SearchService
74
+ pages: PagesService
75
+ topics: TopicsService
76
+ health: HealthService
77
+ diagnostics: DiagnosticsService
78
+ tagging: TaggingService
79
+ updates: UpdatesService
80
+ viewer: ViewerService
81
+ runs: RunsService
82
+ sources: SourcesService
83
+ harnesses: HarnessesService
84
+ prompts: PromptRenderer
85
+ manual: ManualLibrary
86
+ workflows: CodeAlmanacWorkflows
87
+
88
+
89
+ def create_app(
90
+ config: AppConfig | None = None,
91
+ harness_adapters: Sequence[HarnessAdapter] | None = None,
92
+ transcript_discovery_adapters: Sequence[TranscriptDiscoveryAdapter] | None = None,
93
+ source_runtime_adapters: Sequence[SourceRuntimeAdapter] | None = None,
94
+ scheduler: SchedulerAdapter | None = None,
95
+ update_metadata: PackageInstallMetadataProvider | None = None,
96
+ update_runner: PackageCommandRunner | None = None,
97
+ ) -> CodeAlmanac:
98
+ app_config = config or AppConfig()
99
+ workspaces = WorkspacesService(WorkspaceRegistryStore(app_config.registry_path))
100
+ config_service = ConfigService(workspaces, ConfigStore(), app_config.config_path)
101
+ automation = AutomationService(workspaces, scheduler or LaunchdSchedulerAdapter())
102
+ manual = ManualLibrary()
103
+ wiki = WikiService(workspaces, manual)
104
+ index = IndexService(workspaces, IndexStore())
105
+ search = SearchService(workspaces, index)
106
+ pages = PagesService(workspaces, index)
107
+ topics = TopicsService(workspaces, index)
108
+ health = HealthService(workspaces, index)
109
+ diagnostics = DiagnosticsService(workspaces, index, manual, __version__)
110
+ tagging = TaggingService(pages)
111
+ updates = UpdatesService(
112
+ update_metadata or InstalledPackageMetadataProvider(),
113
+ update_runner or SubprocessPackageCommandRunner(),
114
+ )
115
+ viewer = ViewerService(workspaces, index, MarkdownRenderer())
116
+ runs = RunsService(workspaces, RunStore())
117
+ sources = SourcesService(
118
+ default_transcript_discovery_adapters()
119
+ if transcript_discovery_adapters is None
120
+ else transcript_discovery_adapters,
121
+ default_source_runtime_adapters()
122
+ if source_runtime_adapters is None
123
+ else source_runtime_adapters,
124
+ )
125
+ prompts = PromptRenderer()
126
+ harnesses = HarnessesService(
127
+ default_harness_adapters() if harness_adapters is None else harness_adapters
128
+ )
129
+ build = BuildWorkflow(workspaces, wiki, index)
130
+ ingest = IngestWorkflow(
131
+ workspaces,
132
+ sources,
133
+ harnesses,
134
+ runs,
135
+ index,
136
+ LifecycleMutationPolicy(GitWorkspaceChangeProbe(), operation="ingest"),
137
+ prompts,
138
+ )
139
+ garden = GardenWorkflow(
140
+ workspaces,
141
+ harnesses,
142
+ runs,
143
+ index,
144
+ health,
145
+ LifecycleMutationPolicy(GitWorkspaceChangeProbe(), operation="garden"),
146
+ prompts,
147
+ )
148
+ sync = SyncWorkflow(workspaces, sources, runs, ingest, SyncLedgerStore())
149
+ workflows = CodeAlmanacWorkflows(
150
+ build=build,
151
+ ingest=ingest,
152
+ garden=garden,
153
+ sync=sync,
154
+ )
155
+ return CodeAlmanac(
156
+ automation=automation,
157
+ config=config_service,
158
+ workspaces=workspaces,
159
+ wiki=wiki,
160
+ index=index,
161
+ search=search,
162
+ pages=pages,
163
+ topics=topics,
164
+ health=health,
165
+ diagnostics=diagnostics,
166
+ tagging=tagging,
167
+ updates=updates,
168
+ viewer=viewer,
169
+ runs=runs,
170
+ sources=sources,
171
+ harnesses=harnesses,
172
+ prompts=prompts,
173
+ manual=manual,
174
+ workflows=workflows,
175
+ )
@@ -0,0 +1 @@
1
+
File without changes
@@ -0,0 +1,124 @@
1
+ import argparse
2
+ from collections.abc import Sequence
3
+ from datetime import timedelta
4
+ from pathlib import Path
5
+
6
+ from codealmanac.app import CodeAlmanac
7
+ from codealmanac.cli.dispatch.config import (
8
+ load_cli_config,
9
+ parse_optional_duration,
10
+ )
11
+ from codealmanac.cli.render.admin import (
12
+ render_automation_install,
13
+ render_automation_status,
14
+ render_automation_uninstall,
15
+ render_doctor,
16
+ render_run,
17
+ render_run_log,
18
+ render_runs,
19
+ render_update_plan,
20
+ render_update_result,
21
+ )
22
+ from codealmanac.services.automation.models import AutomationTask
23
+ from codealmanac.services.automation.requests import (
24
+ AutomationStatusRequest,
25
+ InstallAutomationRequest,
26
+ UninstallAutomationRequest,
27
+ )
28
+ from codealmanac.services.config.models import CodeAlmanacConfig
29
+ from codealmanac.services.diagnostics.requests import DoctorRequest
30
+ from codealmanac.services.runs.requests import (
31
+ ListRunsRequest,
32
+ ReadRunLogRequest,
33
+ ShowRunRequest,
34
+ )
35
+ from codealmanac.services.updates.models import UpdateStatus
36
+ from codealmanac.services.updates.requests import CheckUpdateRequest, RunUpdateRequest
37
+
38
+ ADMIN_COMMANDS = frozenset(("automation", "doctor", "jobs", "update"))
39
+
40
+
41
+ def is_admin_command(command: str | None) -> bool:
42
+ return command in ADMIN_COMMANDS
43
+
44
+
45
+ def dispatch_admin(args: argparse.Namespace, app: CodeAlmanac) -> int:
46
+ if args.command == "doctor":
47
+ report = app.diagnostics.check(DoctorRequest(cwd=Path.cwd(), wiki=args.wiki))
48
+ render_doctor(report, json_output=args.json)
49
+ return 0
50
+ if args.command == "update":
51
+ if args.check:
52
+ plan = app.updates.check(CheckUpdateRequest())
53
+ render_update_plan(plan, json_output=args.json)
54
+ return 0
55
+ result = app.updates.run(RunUpdateRequest())
56
+ render_update_result(result, json_output=args.json)
57
+ return 0 if result.status == UpdateStatus.COMPLETED else 1
58
+ if args.command == "jobs":
59
+ if args.jobs_command == "show":
60
+ record = app.runs.show(
61
+ ShowRunRequest(cwd=Path.cwd(), wiki=args.wiki, run_id=args.run_id)
62
+ )
63
+ render_run(record, json_output=args.json)
64
+ return 0
65
+ if args.jobs_command == "logs":
66
+ events = app.runs.log(
67
+ ReadRunLogRequest(cwd=Path.cwd(), wiki=args.wiki, run_id=args.run_id)
68
+ )
69
+ render_run_log(events, json_output=args.json)
70
+ return 0
71
+ records = app.runs.list(
72
+ ListRunsRequest(cwd=Path.cwd(), wiki=args.wiki, limit=args.limit)
73
+ )
74
+ render_runs(records, json_output=args.json)
75
+ return 0
76
+ if args.command == "automation":
77
+ tasks = parse_automation_tasks(args.tasks)
78
+ if args.automation_command == "install":
79
+ cli_config = load_cli_config(app, None)
80
+ result = app.automation.install(
81
+ InstallAutomationRequest(
82
+ cwd=Path.cwd(),
83
+ tasks=tasks,
84
+ every=parse_optional_duration(args.every, "--every"),
85
+ quiet=resolve_automation_quiet(args.quiet, cli_config),
86
+ garden_every=parse_optional_duration(
87
+ args.garden_every,
88
+ "--garden-every",
89
+ ),
90
+ garden_off=args.garden_off,
91
+ )
92
+ )
93
+ render_automation_install(result, json_output=args.json)
94
+ return 0
95
+ if args.automation_command == "uninstall":
96
+ result = app.automation.uninstall(UninstallAutomationRequest(tasks=tasks))
97
+ render_automation_uninstall(result, json_output=args.json)
98
+ return 0
99
+ if args.automation_command == "status":
100
+ result = app.automation.status(AutomationStatusRequest(tasks=tasks))
101
+ render_automation_status(result, json_output=args.json)
102
+ return 0
103
+ raise AssertionError(f"unhandled admin command: {args.command}")
104
+
105
+
106
+ def resolve_automation_quiet(
107
+ value: str | None,
108
+ config: CodeAlmanacConfig,
109
+ ) -> timedelta:
110
+ if value is None:
111
+ return config.sync.quiet
112
+ parsed = parse_optional_duration(value, "--quiet")
113
+ if parsed is None:
114
+ raise AssertionError("parsed automation quiet is unexpectedly empty")
115
+ return parsed
116
+
117
+
118
+ def parse_automation_tasks(values: Sequence[str]) -> tuple[AutomationTask, ...]:
119
+ tasks: list[AutomationTask] = []
120
+ for value in values:
121
+ task = AutomationTask(value)
122
+ if task not in tasks:
123
+ tasks.append(task)
124
+ return tuple(tasks)
@@ -0,0 +1,50 @@
1
+ from datetime import timedelta
2
+ from pathlib import Path
3
+
4
+ from humanfriendly import InvalidTimespan, parse_timespan
5
+
6
+ from codealmanac.app import CodeAlmanac
7
+ from codealmanac.core.errors import ValidationFailed
8
+ from codealmanac.services.config.models import CodeAlmanacConfig
9
+ from codealmanac.services.config.requests import LoadConfigRequest
10
+ from codealmanac.services.harnesses.models import HarnessKind
11
+ from codealmanac.workflows.sync.requests import DEFAULT_SYNC_PENDING_TIMEOUT
12
+
13
+
14
+ def load_cli_config(app: CodeAlmanac, wiki: str | None) -> CodeAlmanacConfig:
15
+ return app.config.load(LoadConfigRequest(cwd=Path.cwd(), wiki=wiki))
16
+
17
+
18
+ def resolve_harness(value: str | None, config: CodeAlmanacConfig) -> HarnessKind:
19
+ if value is None:
20
+ return config.harness.default
21
+ return HarnessKind(value)
22
+
23
+
24
+ def resolve_quiet(value: str | None, config: CodeAlmanacConfig) -> timedelta:
25
+ if value is None:
26
+ return config.sync.quiet
27
+ return parse_quiet(value)
28
+
29
+
30
+ def resolve_pending_timeout(value: str | None) -> timedelta:
31
+ parsed = parse_optional_duration(value, "--pending-timeout")
32
+ return parsed or DEFAULT_SYNC_PENDING_TIMEOUT
33
+
34
+
35
+ def parse_quiet(value: str) -> timedelta:
36
+ try:
37
+ seconds = parse_timespan(value)
38
+ except InvalidTimespan as error:
39
+ raise ValidationFailed(f"invalid --quiet value: {value}") from error
40
+ return timedelta(seconds=seconds)
41
+
42
+
43
+ def parse_optional_duration(value: str | None, flag: str) -> timedelta | None:
44
+ if value is None:
45
+ return None
46
+ try:
47
+ seconds = parse_timespan(value)
48
+ except InvalidTimespan as error:
49
+ raise ValidationFailed(f"invalid {flag} value: {value}") from error
50
+ return timedelta(seconds=seconds)
@@ -0,0 +1,328 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from codealmanac.app import CodeAlmanac
6
+ from codealmanac.cli.dispatch.admin import dispatch_admin, is_admin_command
7
+ from codealmanac.cli.dispatch.config import (
8
+ load_cli_config,
9
+ resolve_harness,
10
+ resolve_pending_timeout,
11
+ resolve_quiet,
12
+ )
13
+ from codealmanac.cli.render.root import (
14
+ render_build,
15
+ render_garden,
16
+ render_health,
17
+ render_ingest,
18
+ render_page,
19
+ render_reindex,
20
+ render_search,
21
+ render_sync_status,
22
+ render_tagging,
23
+ render_topic,
24
+ render_topic_edge_mutation,
25
+ render_topic_mutation,
26
+ render_topic_rewrite_mutation,
27
+ render_topics,
28
+ render_workspace_drop,
29
+ render_workspace_list,
30
+ )
31
+ from codealmanac.core.errors import ValidationFailed
32
+ from codealmanac.services.health.requests import HealthCheckRequest
33
+ from codealmanac.services.index.requests import ReindexRequest
34
+ from codealmanac.services.pages.requests import ShowPageRequest
35
+ from codealmanac.services.search.requests import SearchPagesRequest
36
+ from codealmanac.services.sources.models import TranscriptApp
37
+ from codealmanac.services.tagging.requests import TagPageRequest, UntagPageRequest
38
+ from codealmanac.services.topics.requests import (
39
+ CreateTopicRequest,
40
+ DeleteTopicRequest,
41
+ DescribeTopicRequest,
42
+ LinkTopicRequest,
43
+ ListTopicsRequest,
44
+ RenameTopicRequest,
45
+ ShowTopicRequest,
46
+ UnlinkTopicRequest,
47
+ )
48
+ from codealmanac.services.workspaces.requests import (
49
+ DropWorkspaceRequest,
50
+ InitializeWorkspaceRequest,
51
+ )
52
+ from codealmanac.workflows.garden.requests import RunGardenRequest
53
+ from codealmanac.workflows.ingest.requests import RunIngestRequest
54
+ from codealmanac.workflows.sync.requests import (
55
+ DEFAULT_SYNC_MAX_FAILED_ATTEMPTS,
56
+ RunSyncRequest,
57
+ RunSyncStatusRequest,
58
+ )
59
+
60
+
61
+ def dispatch(args: argparse.Namespace, app: CodeAlmanac) -> int:
62
+ if args.command == "init":
63
+ workspace = app.workflows.build.initialize(
64
+ InitializeWorkspaceRequest(
65
+ path=Path(args.path),
66
+ almanac_root=Path(args.root) if args.root is not None else None,
67
+ name=args.name,
68
+ description=args.description,
69
+ )
70
+ )
71
+ print(workspace.name)
72
+ print(
73
+ f"initialized {workspace.almanac_path} "
74
+ f"(registry: {app.workspaces.store.path})",
75
+ file=sys.stderr,
76
+ )
77
+ return 0
78
+ if args.command == "build":
79
+ result = app.workflows.build.build(
80
+ InitializeWorkspaceRequest(
81
+ path=Path(args.path),
82
+ almanac_root=Path(args.root) if args.root is not None else None,
83
+ name=args.name,
84
+ description=args.description,
85
+ )
86
+ )
87
+ render_build(result.workspace.name, result.index)
88
+ return 0
89
+ if args.command == "ingest":
90
+ cli_config = load_cli_config(app, args.wiki)
91
+ result = app.workflows.ingest.run(
92
+ RunIngestRequest(
93
+ cwd=Path.cwd(),
94
+ wiki=args.wiki,
95
+ inputs=tuple(args.inputs),
96
+ harness=resolve_harness(args.using, cli_config),
97
+ title=args.title,
98
+ guidance=args.guidance,
99
+ )
100
+ )
101
+ render_ingest(result)
102
+ return 0
103
+ if args.command == "garden":
104
+ cli_config = load_cli_config(app, args.wiki)
105
+ result = app.workflows.garden.run(
106
+ RunGardenRequest(
107
+ cwd=Path.cwd(),
108
+ wiki=args.wiki,
109
+ harness=resolve_harness(args.using, cli_config),
110
+ title=args.title,
111
+ guidance=args.guidance,
112
+ )
113
+ )
114
+ render_garden(result)
115
+ return 0
116
+ if args.command == "sync" and args.sync_command == "status":
117
+ cli_config = load_cli_config(app, args.wiki)
118
+ result = app.workflows.sync.status(
119
+ RunSyncStatusRequest(
120
+ cwd=Path.cwd(),
121
+ wiki=args.wiki,
122
+ apps=parse_sync_apps(args.source_apps),
123
+ quiet=resolve_quiet(args.quiet, cli_config),
124
+ pending_timeout=resolve_pending_timeout(args.pending_timeout),
125
+ max_failed_attempts=args.max_failed_attempts
126
+ if args.max_failed_attempts is not None
127
+ else DEFAULT_SYNC_MAX_FAILED_ATTEMPTS,
128
+ )
129
+ )
130
+ render_sync_status(result, json_output=args.json)
131
+ return 0
132
+ if args.command == "sync":
133
+ cli_config = load_cli_config(app, args.wiki)
134
+ result = app.workflows.sync.run(
135
+ RunSyncRequest(
136
+ cwd=Path.cwd(),
137
+ wiki=args.wiki,
138
+ apps=parse_sync_apps(args.source_apps),
139
+ quiet=resolve_quiet(args.quiet, cli_config),
140
+ pending_timeout=resolve_pending_timeout(args.pending_timeout),
141
+ max_failed_attempts=args.max_failed_attempts
142
+ if args.max_failed_attempts is not None
143
+ else DEFAULT_SYNC_MAX_FAILED_ATTEMPTS,
144
+ harness=resolve_harness(args.using, cli_config),
145
+ claim_owner=args.claim_owner,
146
+ )
147
+ )
148
+ render_sync_status(result, json_output=args.json)
149
+ return 0
150
+ if args.command == "list":
151
+ if args.drop is not None:
152
+ result = app.workspaces.drop(
153
+ DropWorkspaceRequest(selector=args.drop, base_path=Path.cwd())
154
+ )
155
+ render_workspace_drop(result, json_output=args.json)
156
+ return 0
157
+ if args.drop_missing:
158
+ result = app.workspaces.drop_missing()
159
+ render_workspace_drop(result, json_output=args.json)
160
+ return 0
161
+ render_workspace_list(app.workspaces.list_registry(), json_output=args.json)
162
+ return 0
163
+ if args.command == "search":
164
+ rows = app.search.search(
165
+ SearchPagesRequest(
166
+ cwd=Path.cwd(),
167
+ wiki=args.wiki,
168
+ query=args.query,
169
+ topics=tuple(args.topic),
170
+ mentions=args.mentions,
171
+ include_archive=args.include_archive,
172
+ archived=args.archived,
173
+ limit=args.limit,
174
+ )
175
+ )
176
+ render_search(rows, json_output=args.json)
177
+ return 0
178
+ if args.command == "show":
179
+ page = app.pages.show(
180
+ ShowPageRequest(cwd=Path.cwd(), wiki=args.wiki, slug=args.slug)
181
+ )
182
+ render_page(page, args)
183
+ return 0
184
+ if args.command == "topics":
185
+ if args.topic_command == "show":
186
+ topic = app.topics.show(
187
+ ShowTopicRequest(
188
+ cwd=Path.cwd(),
189
+ wiki=args.wiki,
190
+ slug=args.slug,
191
+ include_descendants=args.descendants,
192
+ )
193
+ )
194
+ render_topic(topic)
195
+ return 0
196
+ if args.topic_command == "create":
197
+ result = app.topics.create(
198
+ CreateTopicRequest(
199
+ cwd=Path.cwd(),
200
+ wiki=args.wiki,
201
+ name=args.name,
202
+ parents=tuple(args.parent),
203
+ )
204
+ )
205
+ render_topic_mutation(result)
206
+ return 0
207
+ if args.topic_command == "describe":
208
+ result = app.topics.describe(
209
+ DescribeTopicRequest(
210
+ cwd=Path.cwd(),
211
+ wiki=args.wiki,
212
+ slug=args.slug,
213
+ description=args.description,
214
+ )
215
+ )
216
+ render_topic_mutation(result)
217
+ return 0
218
+ if args.topic_command == "link":
219
+ result = app.topics.link(
220
+ LinkTopicRequest(
221
+ cwd=Path.cwd(),
222
+ wiki=args.wiki,
223
+ child=args.child,
224
+ parent=args.parent,
225
+ )
226
+ )
227
+ render_topic_edge_mutation(result)
228
+ return 0
229
+ if args.topic_command == "unlink":
230
+ result = app.topics.unlink(
231
+ UnlinkTopicRequest(
232
+ cwd=Path.cwd(),
233
+ wiki=args.wiki,
234
+ child=args.child,
235
+ parent=args.parent,
236
+ )
237
+ )
238
+ render_topic_edge_mutation(result)
239
+ return 0
240
+ if args.topic_command == "rename":
241
+ result = app.topics.rename(
242
+ RenameTopicRequest(
243
+ cwd=Path.cwd(),
244
+ wiki=args.wiki,
245
+ old_slug=args.old_slug,
246
+ new_slug=args.new_slug,
247
+ )
248
+ )
249
+ render_topic_rewrite_mutation(result)
250
+ return 0
251
+ if args.topic_command == "delete":
252
+ result = app.topics.delete(
253
+ DeleteTopicRequest(
254
+ cwd=Path.cwd(),
255
+ wiki=args.wiki,
256
+ slug=args.slug,
257
+ )
258
+ )
259
+ render_topic_rewrite_mutation(result)
260
+ return 0
261
+ topics = app.topics.list(ListTopicsRequest(cwd=Path.cwd(), wiki=args.wiki))
262
+ render_topics(topics)
263
+ return 0
264
+ if args.command == "health":
265
+ report = app.health.check(HealthCheckRequest(cwd=Path.cwd(), wiki=args.wiki))
266
+ render_health(report, json_output=args.json)
267
+ return 0
268
+ if args.command == "reindex":
269
+ result = app.index.reindex(ReindexRequest(cwd=Path.cwd(), wiki=args.wiki))
270
+ render_reindex(result, json_output=args.json)
271
+ return 0
272
+ if is_admin_command(args.command):
273
+ return dispatch_admin(args, app)
274
+ if args.command == "serve":
275
+ return run_serve(app, args)
276
+ if args.command == "tag":
277
+ result = app.tagging.tag(
278
+ TagPageRequest(
279
+ cwd=Path.cwd(),
280
+ wiki=args.wiki,
281
+ slug=args.slug,
282
+ topics=tuple(args.topics),
283
+ )
284
+ )
285
+ render_tagging("tagged", "already tagged", result)
286
+ return 0
287
+ if args.command == "untag":
288
+ result = app.tagging.untag(
289
+ UntagPageRequest(
290
+ cwd=Path.cwd(),
291
+ wiki=args.wiki,
292
+ slug=args.slug,
293
+ topics=tuple(args.topics),
294
+ )
295
+ )
296
+ render_tagging("untagged", "not tagged", result)
297
+ return 0
298
+ raise AssertionError(f"unhandled command: {args.command}")
299
+
300
+
301
+ def parse_sync_apps(value: str | None) -> tuple[TranscriptApp, ...]:
302
+ if value is None or value.strip() == "":
303
+ return (TranscriptApp.CLAUDE, TranscriptApp.CODEX)
304
+ apps: list[TranscriptApp] = []
305
+ for raw in value.split(","):
306
+ item = raw.strip()
307
+ try:
308
+ app = TranscriptApp(item)
309
+ except ValueError as error:
310
+ raise ValidationFailed(
311
+ f'invalid --from "{value}" (expected claude,codex)'
312
+ ) from error
313
+ if app not in apps:
314
+ apps.append(app)
315
+ if len(apps) == 0:
316
+ raise ValidationFailed("at least one sync source is required")
317
+ return tuple(apps)
318
+
319
+
320
+ def run_serve(app, args: argparse.Namespace) -> int:
321
+ import uvicorn
322
+
323
+ from codealmanac.server.app import create_server_app
324
+
325
+ server = create_server_app(app, Path.cwd(), args.wiki)
326
+ print(f"codealmanac viewer: http://{args.host}:{args.port}")
327
+ uvicorn.run(server, host=args.host, port=args.port, log_level="warning")
328
+ return 0