code-data-ark 2.0.3__tar.gz → 2.0.4__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 (35) hide show
  1. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/PKG-INFO +1 -1
  2. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/kernel/pmf_kernel.py +130 -0
  3. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/ui/cli.py +93 -9
  4. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/changelog.md +9 -0
  5. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/pyproject.toml +1 -1
  6. code_data_ark-2.0.4/version +1 -0
  7. code_data_ark-2.0.3/version +0 -1
  8. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/.flake8 +0 -0
  9. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/.github/workflows/ci.yml +0 -0
  10. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/.gitignore +0 -0
  11. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/bin/release.py +0 -0
  12. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/__init__.py +0 -0
  13. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/kernel/__init__.py +0 -0
  14. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/kernel/control_db.py +0 -0
  15. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/kernel/paths.py +0 -0
  16. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/kernel/selfcheck.py +0 -0
  17. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/pipeline/__init__.py +0 -0
  18. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/pipeline/embed.py +0 -0
  19. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/pipeline/extract.py +0 -0
  20. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/pipeline/ingest.py +0 -0
  21. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/pipeline/parse_edits.py +0 -0
  22. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/pipeline/reconstruct.py +0 -0
  23. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/pipeline/watcher.py +0 -0
  24. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/ui/__init__.py +0 -0
  25. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/cda/ui/web.py +0 -0
  26. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/contributing.md +0 -0
  27. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/docs/architecture.md +0 -0
  28. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/docs/examples/usage.md +0 -0
  29. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/docs/pmf_kernel.md +0 -0
  30. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/docs/roadmap.md +0 -0
  31. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/license +0 -0
  32. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/makefile +0 -0
  33. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/readme.md +0 -0
  34. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/tests/test_basic.py +0 -0
  35. {code_data_ark-2.0.3 → code_data_ark-2.0.4}/tests/test_selfcheck.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-data-ark
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Code Data Ark — local observability and intelligence platform for VS Code + Copilot Chat sessions
5
5
  Project-URL: Homepage, https://github.com/goCosmix/cda
6
6
  Project-URL: Repository, https://github.com/goCosmix/cda.git
@@ -1,9 +1,13 @@
1
1
  import json
2
2
  import os
3
+ import shutil
3
4
  import signal
5
+ import socket
4
6
  import subprocess
5
7
  import sys
8
+ import threading
6
9
  import time
10
+ import webbrowser
7
11
  from dataclasses import dataclass
8
12
  from pathlib import Path
9
13
  from typing import Dict, List, Optional
@@ -17,6 +21,132 @@ from cda.kernel.paths import (
17
21
  DEFAULT_HOST = "127.0.0.1"
18
22
  DEFAULT_PORT = 10001
19
23
 
24
+ # ── launchd integration ──────────────────────────────────────────────────────
25
+
26
+ PLIST_LABEL = "com.gocosmix.cda"
27
+
28
+
29
+ def plist_path() -> Path:
30
+ return Path.home() / "Library" / "LaunchAgents" / f"{PLIST_LABEL}.plist"
31
+
32
+
33
+ def generate_plist(cda_bin: str, cda_home: Path) -> str:
34
+ log = cda_home / "logs" / "launchd.log"
35
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
36
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
37
+ <plist version="1.0">
38
+ <dict>
39
+ <key>Label</key>
40
+ <string>{PLIST_LABEL}</string>
41
+ <key>ProgramArguments</key>
42
+ <array>
43
+ <string>{cda_bin}</string>
44
+ <string>pmf</string>
45
+ <string>up</string>
46
+ </array>
47
+ <key>RunAtLoad</key>
48
+ <true/>
49
+ <key>KeepAlive</key>
50
+ <false/>
51
+ <key>StandardOutPath</key>
52
+ <string>{log}</string>
53
+ <key>StandardErrorPath</key>
54
+ <string>{log}</string>
55
+ <key>EnvironmentVariables</key>
56
+ <dict>
57
+ <key>CDA_HOME</key>
58
+ <string>{cda_home}</string>
59
+ <key>PATH</key>
60
+ <string>{os.path.dirname(cda_bin)}:/usr/local/bin:/usr/bin:/bin</string>
61
+ </dict>
62
+ </dict>
63
+ </plist>
64
+ """
65
+
66
+
67
+ def install_launchd(cda_home: Path) -> Path:
68
+ """Write the LaunchAgent plist and load it with launchctl."""
69
+ cda_bin = shutil.which("cda")
70
+ if not cda_bin:
71
+ raise PMFKernelError("cda binary not found on PATH — cannot generate plist")
72
+
73
+ target = plist_path()
74
+ target.parent.mkdir(parents=True, exist_ok=True)
75
+ target.write_text(generate_plist(cda_bin, cda_home))
76
+
77
+ # Unload any stale registration first
78
+ subprocess.run(["launchctl", "unload", str(target)], capture_output=True)
79
+
80
+ result = subprocess.run(
81
+ ["launchctl", "load", str(target)],
82
+ capture_output=True,
83
+ text=True,
84
+ )
85
+ if result.returncode != 0:
86
+ raise PMFKernelError(f"launchctl load failed: {result.stderr.strip()}")
87
+
88
+ return target
89
+
90
+
91
+ def uninstall_launchd() -> None:
92
+ """Unload and remove the LaunchAgent plist."""
93
+ target = plist_path()
94
+ if target.exists():
95
+ subprocess.run(["launchctl", "unload", str(target)], capture_output=True)
96
+ target.unlink(missing_ok=True)
97
+
98
+
99
+ def open_browser_when_ready(
100
+ url: str,
101
+ host: str = DEFAULT_HOST,
102
+ port: int = DEFAULT_PORT,
103
+ timeout: float = 12.0,
104
+ ) -> threading.Thread:
105
+ """
106
+ Spawn a daemon thread that polls host:port and opens a browser when ready.
107
+ For foreground (serve) use: the thread outlives the caller because serve blocks.
108
+ For background (pmf up / ui start): call wait_for_port() instead so we poll
109
+ synchronously before the process exits.
110
+ """
111
+ def _wait_and_open():
112
+ elapsed = 0.0
113
+ while elapsed < timeout:
114
+ try:
115
+ with socket.create_connection((host, port), timeout=0.5):
116
+ webbrowser.open(url)
117
+ return
118
+ except OSError:
119
+ time.sleep(0.25)
120
+ elapsed += 0.25
121
+
122
+ t = threading.Thread(target=_wait_and_open, daemon=True)
123
+ t.start()
124
+ return t
125
+
126
+
127
+ def wait_for_port_and_open_browser(
128
+ url: str,
129
+ host: str = DEFAULT_HOST,
130
+ port: int = DEFAULT_PORT,
131
+ timeout: float = 8.0,
132
+ ) -> bool:
133
+ """
134
+ Block until host:port accepts connections (or timeout), then open browser.
135
+ Use this when the caller process will exit after starting a background service.
136
+ Returns True if port came up, False on timeout.
137
+ """
138
+ elapsed = 0.0
139
+ while elapsed < timeout:
140
+ try:
141
+ with socket.create_connection((host, port), timeout=0.5):
142
+ webbrowser.open(url)
143
+ return True
144
+ except OSError:
145
+ time.sleep(0.25)
146
+ elapsed += 0.25
147
+ return False
148
+
149
+
20
150
  ensure_dirs()
21
151
 
22
152
 
@@ -24,6 +24,9 @@ Commands:
24
24
  cda pmf stop <service> Stop a service
25
25
  cda pmf restart <service> Restart a service
26
26
  cda pmf logs <service> Tail service logs
27
+ cda pmf up Start watcher + web UI (opens browser when ready)
28
+ cda pmf install Register as macOS LaunchAgent (auto-start on login)
29
+ cda pmf uninstall Remove the LaunchAgent registration
27
30
  cda check Run a full self-diagnostic. The system checks itself.
28
31
  cda init First-run setup — create ~/.cda/ and validate environment
29
32
  cda serve Start the local web UI on port 10001
@@ -62,7 +65,11 @@ import textwrap
62
65
  import datetime
63
66
  from pathlib import Path
64
67
  from cda.pipeline.reconstruct import decompress_vfs
65
- from cda.kernel.pmf_kernel import PMFKernel, PMFKernelError
68
+ from cda.kernel.pmf_kernel import (
69
+ PMFKernel, PMFKernelError,
70
+ install_launchd, uninstall_launchd, plist_path,
71
+ open_browser_when_ready, wait_for_port_and_open_browser,
72
+ )
66
73
  from cda.kernel.paths import (
67
74
  DB_PATH, PID_FILE, UI_PID_FILE, UI_LOG_FILE,
68
75
  QUEUE_DIR, POLICY_FILE, ensure_dirs,
@@ -361,10 +368,11 @@ def status():
361
368
  @cli.command("serve")
362
369
  @click.option("--host", default="127.0.0.1", show_default=True, help="Local host to bind the web UI")
363
370
  @click.option("--port", default=10001, show_default=True, help="Local port for the web UI")
364
- def serve(host, port):
371
+ @click.option("--no-browser", "no_browser", is_flag=True, default=False, help="Don't open browser automatically")
372
+ def serve(host, port, no_browser):
365
373
  """Start the local web UI for Code Data Ark in the foreground."""
366
- click.echo(yellow(f" Starting local web UI at http://{host}:{port}"))
367
- click.echo(yellow(" Use `cda ui start` to launch it as a background service."))
374
+ url = f"http://{host}:{port}"
375
+ click.echo(yellow(f" Starting local web UI at {url}"))
368
376
  try:
369
377
  import importlib
370
378
  import cda.ui.web as web
@@ -373,6 +381,8 @@ def serve(host, port):
373
381
  click.echo(red(" Failed to start web UI. Ensure the package is installed and importable."))
374
382
  click.echo(red(f" Details: {exc}"))
375
383
  return
384
+ if not no_browser:
385
+ open_browser_when_ready(url, host, port)
376
386
  web.start_server(host=host, port=port)
377
387
 
378
388
 
@@ -396,12 +406,17 @@ def _ui_is_running():
396
406
  @ui.command("start")
397
407
  @click.option("--host", default="127.0.0.1", show_default=True, help="Local host to bind the web UI")
398
408
  @click.option("--port", default=10001, show_default=True, help="Local port for the web UI")
399
- def ui_start(host, port):
409
+ @click.option("--no-browser", "no_browser", is_flag=True, default=False, help="Don't open browser automatically")
410
+ def ui_start(host, port, no_browser):
400
411
  """Start the web UI as a background service."""
401
412
  try:
402
413
  result = kernel.start_service("ui", options={"host": host, "port": port})
403
- click.echo(green(f" Web UI started in background at http://{host}:{port} pid={result['pid']}"))
414
+ url = f"http://{host}:{port}"
415
+ click.echo(green(f" Web UI started in background at {url} pid={result['pid']}"))
404
416
  click.echo(yellow(f" Logs: {UI_LOG_FILE}"))
417
+ if not no_browser:
418
+ click.echo(dim(" Opening browser when server is ready..."))
419
+ wait_for_port_and_open_browser(url, host, port)
405
420
  except PMFKernelError as exc:
406
421
  click.echo(red(f" Failed to start UI: {exc}"))
407
422
 
@@ -485,12 +500,17 @@ def pmf_status(service_id):
485
500
  @click.argument("service_id")
486
501
  @click.option("--host", default="127.0.0.1", help="Host override for UI service")
487
502
  @click.option("--port", default=10001, help="Port override for UI service")
488
- def pmf_start(service_id, host, port):
503
+ @click.option("--no-browser", "no_browser", is_flag=True, default=False, help="Don't open browser (UI service only)")
504
+ def pmf_start(service_id, host, port, no_browser):
489
505
  """Start a PMF-managed Ark service."""
490
506
  options = {"host": host, "port": port} if service_id == "ui" else None
491
507
  try:
492
508
  result = kernel.start_service(service_id, options=options)
493
509
  click.echo(green(f" Started {result['label']} pid={result['pid']}"))
510
+ if service_id == "ui" and not no_browser:
511
+ url = f"http://{host}:{port}"
512
+ click.echo(dim(" Opening browser when server is ready..."))
513
+ wait_for_port_and_open_browser(url, host, port)
494
514
  except PMFKernelError as exc:
495
515
  click.echo(red(f" {exc}"))
496
516
 
@@ -529,6 +549,69 @@ def pmf_logs(service_id, tail):
529
549
  click.echo(red(f" {exc}"))
530
550
 
531
551
 
552
+ @pmf.command("up")
553
+ @click.option("--host", default="127.0.0.1", show_default=True, help="Host for web UI")
554
+ @click.option("--port", default=10001, show_default=True, help="Port for web UI")
555
+ @click.option("--no-browser", "no_browser", is_flag=True, default=False, help="Don't open browser when UI is ready")
556
+ def pmf_up(host, port, no_browser):
557
+ """Start all CDA services (watcher + web UI). Called automatically by launchd on login."""
558
+ url = f"http://{host}:{port}"
559
+
560
+ click.echo(bold(" Code Data Ark — starting services"))
561
+ click.echo(hr())
562
+
563
+ try:
564
+ result = kernel.start_service("watcher")
565
+ click.echo(green(f" Watcher started pid={result['pid']}"))
566
+ except PMFKernelError as exc:
567
+ click.echo(yellow(f" Watcher {exc}"))
568
+
569
+ try:
570
+ result = kernel.start_service("ui", options={"host": host, "port": port})
571
+ click.echo(green(f" Web UI started pid={result['pid']} → {url}"))
572
+ if not no_browser:
573
+ click.echo(dim(" Opening browser when server is ready..."))
574
+ wait_for_port_and_open_browser(url, host, port)
575
+ except PMFKernelError as exc:
576
+ click.echo(yellow(f" Web UI {exc}"))
577
+
578
+ click.echo()
579
+
580
+
581
+ @pmf.command("install")
582
+ def pmf_install():
583
+ """Install CDA as a macOS launchd LaunchAgent (auto-start on login)."""
584
+ from cda.kernel.paths import CDA_HOME as _cda_home
585
+ click.echo()
586
+ click.echo(bold(" Installing CDA LaunchAgent"))
587
+ click.echo(hr())
588
+ try:
589
+ target = install_launchd(_cda_home)
590
+ click.echo(green(f" Plist: {target}"))
591
+ click.echo(green(" Label: com.gocosmix.cda"))
592
+ click.echo(green(" Loaded: yes — CDA will start automatically on next login"))
593
+ click.echo()
594
+ click.echo(dim(" To start services now without logging out:"))
595
+ click.echo(dim(" cda pmf up"))
596
+ click.echo()
597
+ except PMFKernelError as exc:
598
+ click.echo(red(f" {exc}"))
599
+ click.echo(yellow(" Make sure `cda` is on PATH: export PATH=\"$HOME/Library/Python/3.9/bin:$PATH\""))
600
+ click.echo()
601
+
602
+
603
+ @pmf.command("uninstall")
604
+ def pmf_uninstall():
605
+ """Remove the CDA launchd LaunchAgent."""
606
+ target = plist_path()
607
+ if not target.exists():
608
+ click.echo(yellow(" No LaunchAgent plist found — nothing to uninstall."))
609
+ return
610
+ uninstall_launchd()
611
+ click.echo(green(f" Removed: {target}"))
612
+ click.echo(green(" CDA will no longer start automatically on login."))
613
+
614
+
532
615
  @cli.group()
533
616
  def embed():
534
617
  """Build and inspect semantic intelligence."""
@@ -2606,9 +2689,10 @@ def init():
2606
2689
  click.echo(bold(" CDA_HOME: ") + str(CDA_HOME))
2607
2690
  click.echo()
2608
2691
  click.echo(dim(" Next steps:"))
2692
+ click.echo(dim(" cda pmf install — register as a macOS LaunchAgent (auto-start on login)"))
2609
2693
  click.echo(dim(" cda sync — ingest all VS Code session data"))
2610
- click.echo(dim(" cda watch start — start the live watcher daemon"))
2611
- click.echo(dim(" cda serve — open the web dashboard on :10001"))
2694
+ click.echo(dim(" cda pmf up — start watcher + web UI now (opens browser)"))
2695
+ click.echo(dim(" cda serve — run web UI in foreground (opens browser)"))
2612
2696
  click.echo()
2613
2697
 
2614
2698
 
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.4] - 2026-05-11
9
+
10
+ ### Added
11
+ - **`cda pmf install`** — generates and loads a macOS `~/Library/LaunchAgents/com.gocosmix.cda.plist`; CDA starts automatically on login
12
+ - **`cda pmf uninstall`** — unloads and removes the LaunchAgent plist
13
+ - **`cda pmf up`** — starts watcher + web UI in one command; opens browser when the server is ready; called by launchd on login
14
+ - **Browser auto-open**: `cda serve`, `cda ui start`, and `cda pmf start ui` now open a browser tab when the server is ready (`--no-browser` to disable)
15
+ - `cda.kernel.pmf_kernel`: `install_launchd()`, `uninstall_launchd()`, `generate_plist()`, `plist_path()`, `open_browser_when_ready()`, `wait_for_port_and_open_browser()`
16
+
8
17
  ## [2.0.3] - 2026-05-11
9
18
 
10
19
  ### Fixed
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-data-ark"
7
- version = "2.0.3"
7
+ version = "2.0.4"
8
8
  description = "Code Data Ark — local observability and intelligence platform for VS Code + Copilot Chat sessions"
9
9
  readme = "readme.md"
10
10
  license = "MIT"
@@ -0,0 +1 @@
1
+ 2.0.4
@@ -1 +0,0 @@
1
- 2.0.3
File without changes
File without changes
File without changes
File without changes
File without changes