code-data-ark 2.0.5__tar.gz → 2.0.7__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 (36) hide show
  1. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/PKG-INFO +17 -8
  2. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/__init__.py +1 -1
  3. code_data_ark-2.0.7/cda/__main__.py +10 -0
  4. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/kernel/paths.py +36 -6
  5. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/kernel/pmf_kernel.py +8 -0
  6. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/ui/cli.py +62 -16
  7. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/ui/web.py +110 -46
  8. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/changelog.md +15 -0
  9. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/pyproject.toml +1 -1
  10. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/readme.md +16 -7
  11. code_data_ark-2.0.7/version +1 -0
  12. code_data_ark-2.0.5/version +0 -1
  13. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/.flake8 +0 -0
  14. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/.github/workflows/ci.yml +0 -0
  15. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/.gitignore +0 -0
  16. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/bin/release.py +0 -0
  17. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/kernel/__init__.py +0 -0
  18. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/kernel/control_db.py +0 -0
  19. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/kernel/selfcheck.py +0 -0
  20. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/pipeline/__init__.py +0 -0
  21. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/pipeline/embed.py +0 -0
  22. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/pipeline/extract.py +0 -0
  23. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/pipeline/ingest.py +0 -0
  24. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/pipeline/parse_edits.py +0 -0
  25. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/pipeline/reconstruct.py +0 -0
  26. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/pipeline/watcher.py +0 -0
  27. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/cda/ui/__init__.py +0 -0
  28. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/contributing.md +0 -0
  29. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/docs/architecture.md +0 -0
  30. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/docs/examples/usage.md +0 -0
  31. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/docs/pmf_kernel.md +0 -0
  32. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/docs/roadmap.md +0 -0
  33. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/license +0 -0
  34. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/makefile +0 -0
  35. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/tests/test_basic.py +0 -0
  36. {code_data_ark-2.0.5 → code_data_ark-2.0.7}/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.5
3
+ Version: 2.0.7
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
@@ -95,10 +95,17 @@ The runtime is managed by an embedded process kernel (PMF) that supervises the w
95
95
  pip install code-data-ark
96
96
  ```
97
97
 
98
+ > **macOS / system Python note**: pip installs the `cda` binary to `~/Library/Python/3.x/bin/` which is not on `PATH` by default. Use the fallback below — `cda setup` will fix PATH for you automatically:
99
+ >
100
+ > ```bash
101
+ > python3 -m cda setup
102
+ > ```
103
+
98
104
  ### Install with pipx
99
105
 
100
106
  ```bash
101
107
  pipx install code-data-ark
108
+ # pipx automatically manages PATH — `cda setup` works immediately
102
109
  ```
103
110
 
104
111
  ### Install from source
@@ -117,25 +124,27 @@ pip install -e ".[dev]"
117
124
  make install-dev
118
125
  ```
119
126
 
120
- > The `cda` console command is installed into your active Python environment's `bin` directory. Activate your virtual environment before running `cda`.
127
+ > The `cda` console command is installed into your Python environment's `bin` directory. If it isn't on PATH yet, use `python3 -m cda setup` — setup patches `~/.zprofile` automatically.
121
128
 
122
129
  ## ⚡ Quick Start
123
130
 
124
131
  ```bash
125
132
  pip install code-data-ark
126
- cda setup
133
+ python3 -m cda setup # use this if `cda` isn't on PATH yet
127
134
  ```
128
135
 
129
- That's it. `cda setup` runs four steps in sequence:
136
+ After the first run, `cda setup` patches `~/.zprofile` so `cda` is on PATH in every new terminal.
137
+
138
+ `cda setup` runs four steps in sequence:
130
139
 
131
140
  | Step | What it does |
132
141
  |------|-------------|
133
- | **1. Init** | Creates `~/.cda/` directory tree, validates your VS Code data path |
134
- | **2. PMF install** | Registers a macOS LaunchAgent — CDA starts automatically on every login |
135
- | **3. Sync** | Ingests all VS Code + Copilot session data into `~/.cda/data/cda.db` |
142
+ | **1. Init** | Creates `~/Library/goCosmix/apps/code-data-ark/` all app data in one organized namespace. Also patches `~/.zprofile` if `cda` isn't on PATH yet. |
143
+ | **2. PMF install** | Registers a macOS LaunchAgent — CDA starts automatically on every login via `cda pmf up` |
144
+ | **3. Sync** | Ingests all VS Code + Copilot session data into `cda.db` |
136
145
  | **4. Up** | Starts the watcher daemon and web UI via the PMF kernel, opens browser |
137
146
 
138
- After setup, everything is managed by the **PMF kernel**. On every login, `launchd` calls `cda pmf up` which starts the watcher and web UI. No terminal interaction required.
147
+ All data lives in `~/Library/goCosmix/apps/code-data-ark/`. After setup, everything is managed by the **PMF kernel** no terminal interaction required.
139
148
 
140
149
  ### Options
141
150
 
@@ -1,3 +1,3 @@
1
1
  """Code Data Ark — local observability and intelligence platform for VS Code + Copilot Chat sessions."""
2
2
 
3
- __version__ = "2.0.2"
3
+ __version__ = "2.0.7"
@@ -0,0 +1,10 @@
1
+ """
2
+ Allows `python3 -m cda <command>` as a fallback when the `cda` binary
3
+ is not yet on PATH (e.g. immediately after `pip install code-data-ark`).
4
+
5
+ python3 -m cda setup
6
+ """
7
+ from cda.ui.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -5,10 +5,16 @@ CDA_HOME is the single root for all runtime state (DB, PID files, logs,
5
5
  queue, PMF runtime). It is resolved exactly once at import time via:
6
6
 
7
7
  1. CDA_HOME environment variable (absolute path)
8
- 2. ~/.cda/ (default — survives pip install, editable install, CI)
8
+ 2. ~/Library/goCosmix/apps/code-data-ark/ (macOS default)
9
+ 3. ~/.gocosmix/apps/code-data-ark/ (fallback on non-macOS)
9
10
 
10
- Pipeline stages and the CLI all import from here so every module agrees
11
- on the same paths regardless of where the package is installed.
11
+ All goCosmix apps share the ~/Library/goCosmix/ namespace:
12
+
13
+ ~/Library/goCosmix/
14
+ ├── apps/
15
+ │ ├── code-data-ark/ ← CDA_HOME
16
+ │ └── ... ← future goCosmix apps
17
+ └── system/ ← shared goCosmix infrastructure
12
18
  """
13
19
 
14
20
  import os
@@ -17,17 +23,40 @@ from pathlib import Path
17
23
  # ── home resolution ──────────────────────────────────────────────────────────
18
24
 
19
25
 
26
+ def _default_cda_home() -> Path:
27
+ """Platform-appropriate default for CDA_HOME."""
28
+ library = Path.home() / "Library"
29
+ if library.exists(): # macOS
30
+ return library / "goCosmix" / "apps" / "code-data-ark"
31
+ return Path.home() / ".gocosmix" / "apps" / "code-data-ark"
32
+
33
+
20
34
  def get_cda_home() -> Path:
21
35
  """Return the CDA home directory, creating it if it doesn't exist."""
22
36
  env = os.environ.get("CDA_HOME")
23
37
  if env:
24
38
  home = Path(env).expanduser().resolve()
25
39
  else:
26
- home = Path.home() / ".cda"
40
+ home = _default_cda_home()
27
41
  home.mkdir(parents=True, exist_ok=True)
28
42
  return home
29
43
 
30
44
 
45
+ # ── goCosmix namespace (shared across all goCosmix apps) ────────────────────
46
+
47
+ def get_gocosmix_home() -> Path:
48
+ """Return ~/Library/goCosmix (macOS) or ~/.gocosmix (other)."""
49
+ library = Path.home() / "Library"
50
+ if library.exists():
51
+ return library / "goCosmix"
52
+ return Path.home() / ".gocosmix"
53
+
54
+
55
+ GOCOSMIX_HOME = get_gocosmix_home()
56
+ GOCOSMIX_APPS = GOCOSMIX_HOME / "apps"
57
+ GOCOSMIX_SYSTEM = GOCOSMIX_HOME / "system"
58
+
59
+
31
60
  # ── canonical paths (module-level constants, computed once) ─────────────────
32
61
 
33
62
  CDA_HOME = get_cda_home()
@@ -49,6 +78,7 @@ RUNTIME_FILE = PMF_DIR / "runtime.json"
49
78
 
50
79
 
51
80
  def ensure_dirs() -> None:
52
- """Create all runtime directories. Safe to call multiple times."""
53
- for d in (DATA_DIR, RUN_DIR, LOG_DIR, QUEUE_DIR, PMF_DIR, PMF_LOG_DIR, CONFIG_DIR):
81
+ """Create all runtime directories (including goCosmix namespace). Safe to call multiple times."""
82
+ for d in (GOCOSMIX_HOME, GOCOSMIX_APPS, GOCOSMIX_SYSTEM,
83
+ DATA_DIR, RUN_DIR, LOG_DIR, QUEUE_DIR, PMF_DIR, PMF_LOG_DIR, CONFIG_DIR):
54
84
  d.mkdir(parents=True, exist_ok=True)
@@ -421,6 +421,14 @@ class PMFKernel:
421
421
  pass
422
422
  time.sleep(0.25)
423
423
  wait_seconds += 0.25
424
+ # Process didn't write its own pid file — write it now using the
425
+ # spawned process's PID so status checks work correctly.
426
+ if self._is_process_alive(proc.pid):
427
+ try:
428
+ spec.pid_file.write_text(str(proc.pid))
429
+ state["pid"] = proc.pid
430
+ except Exception:
431
+ pass
424
432
 
425
433
  if spec.service_type == "daemon":
426
434
  state["status"] = "running"
@@ -28,7 +28,7 @@ Commands:
28
28
  cda pmf install Register as macOS LaunchAgent (auto-start on login)
29
29
  cda pmf uninstall Remove the LaunchAgent registration
30
30
  cda check Run a full self-diagnostic. The system checks itself.
31
- cda init First-run setup — create ~/.cda/ and validate environment
31
+ cda init First-run setup — create ~/Library/goCosmix/ and validate environment
32
32
  cda setup Full onboarding: init → pmf install → sync → up (browser opens)
33
33
  cda serve Start the local web UI on port 10001
34
34
  cda sync Full re-ingest from disk (rebuilds entire DB)
@@ -2671,13 +2671,14 @@ def setup(skip_sync, no_browser):
2671
2671
  Full onboarding in four steps: init → pmf install → sync → up.
2672
2672
 
2673
2673
  \b
2674
- Run this once after `pip install code-data-ark`:
2674
+ Run this once after `pip install code-data-ark`.
2675
+ If `cda` isn't on PATH yet, use the fallback:
2675
2676
 
2676
- cda setup
2677
+ python3 -m cda setup
2677
2678
 
2678
2679
  \b
2679
2680
  What each step does:
2680
- 1. Init — create ~/.cda/ directory tree, validate VS Code data path
2681
+ 1. Init — create ~/Library/goCosmix/apps/code-data-ark/, patch PATH
2681
2682
  2. Install — register a macOS LaunchAgent so CDA starts on every login
2682
2683
  3. Sync — ingest all VS Code + Copilot session data into cda.db
2683
2684
  4. Up — start the watcher daemon and web UI via PMF, open browser
@@ -2685,11 +2686,13 @@ def setup(skip_sync, no_browser):
2685
2686
  All processes are managed by the PMF kernel. The LaunchAgent calls
2686
2687
  `cda pmf up` on every login — no manual interaction needed after setup.
2687
2688
  """
2689
+ import shutil as _shutil
2690
+ import os as _os
2688
2691
  from cda.kernel.paths import (
2689
2692
  CDA_HOME, DATA_DIR, RUN_DIR, LOG_DIR, QUEUE_DIR,
2690
2693
  PMF_DIR, PMF_LOG_DIR, CONFIG_DIR, POLICY_FILE,
2694
+ GOCOSMIX_HOME, GOCOSMIX_APPS, GOCOSMIX_SYSTEM,
2691
2695
  )
2692
- import os as _os
2693
2696
 
2694
2697
  W = 52
2695
2698
  BAR = "═" * W
@@ -2715,14 +2718,28 @@ def setup(skip_sync, no_browser):
2715
2718
  click.echo(bold(bar))
2716
2719
  click.echo()
2717
2720
 
2718
- dirs = [DATA_DIR, RUN_DIR, LOG_DIR, QUEUE_DIR, PMF_DIR, PMF_LOG_DIR, CONFIG_DIR]
2721
+ # Create full goCosmix namespace + app dirs
2722
+ dirs = [GOCOSMIX_HOME, GOCOSMIX_APPS, GOCOSMIX_SYSTEM,
2723
+ DATA_DIR, RUN_DIR, LOG_DIR, QUEUE_DIR, PMF_DIR, PMF_LOG_DIR, CONFIG_DIR]
2719
2724
  for d in dirs:
2725
+ existed = d.exists()
2720
2726
  d.mkdir(parents=True, exist_ok=True)
2721
- click.echo(f" {green('✓')} {d}")
2727
+ if not existed:
2728
+ click.echo(f" {green('+')} {d}")
2729
+ else:
2730
+ click.echo(f" {green('✓')} {d}")
2722
2731
 
2723
2732
  if not POLICY_FILE.exists():
2724
2733
  POLICY_FILE.write_text("# CDA access policy\n# ALLOW <pattern>\n# DENY <pattern>\n")
2725
2734
 
2735
+ # Offer migration from legacy ~/.cda/
2736
+ legacy = Path.home() / ".cda"
2737
+ if legacy.exists() and legacy != CDA_HOME:
2738
+ click.echo()
2739
+ click.echo(yellow(f" ⚠ Legacy data found at {legacy}"))
2740
+ click.echo(yellow(" Run `cda migrate-home` after setup to move it to the new location."))
2741
+
2742
+ # VS Code data dir check
2726
2743
  vscode_data = Path(_os.environ.get(
2727
2744
  "VSCODE_DATA_DIR",
2728
2745
  Path.home() / "Library/Application Support/Code/User",
@@ -2737,25 +2754,54 @@ def setup(skip_sync, no_browser):
2737
2754
  click.echo(f" {green('✓')} CDA_HOME: {CDA_HOME}")
2738
2755
  click.echo()
2739
2756
 
2757
+ # ── PATH patch ───────────────────────────────────────────────
2758
+ # Detect where pip placed the `cda` binary and ensure it's on PATH.
2759
+ # Works whether invoked as `cda setup` or `python3 -m cda setup`.
2760
+ cda_bin_dir = None
2761
+ cda_bin = _shutil.which("cda")
2762
+ if cda_bin:
2763
+ cda_bin_dir = str(Path(cda_bin).parent)
2764
+ else:
2765
+ # pip install --user puts scripts next to python executable
2766
+ py_bin_dir = Path(sys.executable).parent
2767
+ candidate = py_bin_dir / "cda"
2768
+ if candidate.exists():
2769
+ cda_bin_dir = str(py_bin_dir)
2770
+
2771
+ if cda_bin_dir and cda_bin_dir not in _os.environ.get("PATH", "").split(":"):
2772
+ export_line = f'export PATH="{cda_bin_dir}:$PATH"'
2773
+ zprofile = Path.home() / ".zprofile"
2774
+ existing = zprofile.read_text() if zprofile.exists() else ""
2775
+ if export_line not in existing:
2776
+ with open(zprofile, "a") as f:
2777
+ f.write(f"\n# goCosmix — added by cda setup\n{export_line}\n")
2778
+ click.echo(f" {green('+')} PATH updated in ~/.zprofile")
2779
+ click.echo(yellow(" Run `source ~/.zprofile` or open a new terminal to activate."))
2780
+ click.echo(f" {green('✓')} cda binary: {cda_bin_dir}/cda")
2781
+ elif cda_bin:
2782
+ click.echo(f" {green('✓')} cda binary on PATH: {cda_bin}")
2783
+
2784
+ click.echo()
2785
+
2740
2786
  # ── Step 2: PMF install ──────────────────────────────────────
2741
2787
  click.echo(bold(bar))
2742
2788
  click.echo(bold(" Step 2/4 — PMF install"))
2743
2789
  click.echo(bold(bar))
2744
2790
  click.echo()
2745
2791
  click.echo(dim(" The LaunchAgent registers CDA with macOS launchd. On every login,"))
2746
- click.echo(dim(" launchd calls `cda pmf up` which starts the watcher daemon and"))
2747
- click.echo(dim(" web UI via the PMF kernel — no terminal required."))
2792
+ click.echo(dim(" launchd calls `cda pmf up` starts watcher + web UI via PMF kernel."))
2793
+ click.echo(dim(" No terminal required after this."))
2748
2794
  click.echo()
2749
2795
 
2750
2796
  pmf_ok = False
2751
2797
  try:
2752
2798
  target = install_launchd(CDA_HOME)
2753
2799
  click.echo(f" {green('✓')} LaunchAgent: {target}")
2754
- click.echo(f" {green('✓')} Loaded — CDA will start automatically on every login")
2800
+ click.echo(f" {green('✓')} Loaded — CDA starts automatically on every login")
2755
2801
  pmf_ok = True
2756
2802
  except PMFKernelError as exc:
2757
2803
  click.echo(f" {yellow('⚠')} LaunchAgent registration failed: {exc}")
2758
- click.echo(yellow(" Ensure `cda` is on PATH, then run `cda pmf install` to retry."))
2804
+ click.echo(yellow(" Fix PATH then run `cda pmf install` to retry."))
2759
2805
  click.echo()
2760
2806
 
2761
2807
  # ── Step 3: Sync ─────────────────────────────────────────────
@@ -2769,7 +2815,7 @@ def setup(skip_sync, no_browser):
2769
2815
  click.echo()
2770
2816
  else:
2771
2817
  click.echo(dim(" Scanning VS Code workspaceStorage and building cda.db."))
2772
- click.echo(dim(" First run may take several minutes depending on session history."))
2818
+ click.echo(dim(" First run may take a few minutes depending on session history."))
2773
2819
  click.echo()
2774
2820
 
2775
2821
  sync_failed = False
@@ -2833,15 +2879,15 @@ def setup(skip_sync, no_browser):
2833
2879
  click.echo(bold(BAR))
2834
2880
  click.echo()
2835
2881
  if pmf_ok:
2836
- click.echo(dim(" CDA will start automatically on every login via launchd."))
2837
- click.echo(dim(" The watcher daemon stays in sync with your VS Code sessions."))
2882
+ click.echo(dim(" CDA starts automatically on every login via launchd."))
2883
+ click.echo(dim(" The watcher daemon keeps your session data in sync automatically."))
2838
2884
  click.echo(dim(f" Visit {url} any time to explore your data."))
2839
2885
  click.echo()
2840
2886
  click.echo(dim(" Useful commands:"))
2841
2887
  click.echo(dim(" cda check — full system health diagnostic"))
2842
2888
  click.echo(dim(" cda sync — re-ingest after significant new session activity"))
2843
2889
  click.echo(dim(" cda pmf services — view running services and their status"))
2844
- click.echo(dim(" cda pmf uninstall — remove auto-start LaunchAgent registration"))
2890
+ click.echo(dim(" cda pmf uninstall — remove the auto-start LaunchAgent"))
2845
2891
  click.echo()
2846
2892
 
2847
2893
 
@@ -2851,7 +2897,7 @@ def setup(skip_sync, no_browser):
2851
2897
 
2852
2898
  @cli.command("init")
2853
2899
  def init():
2854
- """First-run setup — create ~/.cda/ directory structure and validate environment."""
2900
+ """First-run setup — create ~/Library/goCosmix/apps/code-data-ark/ directory structure and validate environment."""
2855
2901
  from cda.kernel.paths import (
2856
2902
  CDA_HOME, DATA_DIR, RUN_DIR, LOG_DIR, QUEUE_DIR,
2857
2903
  PMF_DIR, PMF_LOG_DIR, CONFIG_DIR, POLICY_FILE,
@@ -1008,7 +1008,7 @@ def get_overview():
1008
1008
  {("(SELECT AVG(heat_score) FROM session_analysis WHERE heat_score IS NOT NULL)" if has_analysis else "0")} as avg_heat,
1009
1009
  {("(SELECT COUNT(*) FROM session_analysis WHERE heat_score >= 50)" if has_analysis else "0")} as critical_sessions,
1010
1010
  {("(SELECT COUNT(*) FROM anomaly_alerts)" if has_alerts else "0")} as alert_count,
1011
- (SELECT COUNT(DISTINCT workspace_id) FROM sessions) as workspace_count,
1011
+ (SELECT COUNT(*) FROM workspaces) as workspace_count,
1012
1012
  (SELECT MAX(created_at) FROM sessions) as last_session
1013
1013
  """)
1014
1014
 
@@ -1041,10 +1041,11 @@ def get_overview():
1041
1041
  LIMIT 15
1042
1042
  """)) if has_signals else []
1043
1043
 
1044
+ exchange_count_expr = "(SELECT COUNT(*) FROM exchanges WHERE exchanges.session_id = s.session_id)" if has_exchanges else "0"
1044
1045
  if has_analysis:
1045
- recent = safe_rows(query_rows("""
1046
+ recent = safe_rows(query_rows(f"""
1046
1047
  SELECT s.session_id as id, s.title, sa.heat_score,
1047
- {("(SELECT COUNT(*) FROM exchanges WHERE exchanges.session_id = s.session_id)" if has_exchanges else "0")} as exchange_count,
1048
+ {exchange_count_expr} as exchange_count,
1048
1049
  s.created_at
1049
1050
  FROM sessions s
1050
1051
  LEFT JOIN session_analysis sa ON sa.session_id = s.session_id
@@ -1052,9 +1053,9 @@ def get_overview():
1052
1053
  LIMIT 10
1053
1054
  """))
1054
1055
  else:
1055
- recent = safe_rows(query_rows("""
1056
+ recent = safe_rows(query_rows(f"""
1056
1057
  SELECT s.session_id as id, s.title, NULL as heat_score,
1057
- {("(SELECT COUNT(*) FROM exchanges WHERE exchanges.session_id = s.session_id)" if has_exchanges else "0")} as exchange_count,
1058
+ {exchange_count_expr} as exchange_count,
1058
1059
  s.created_at
1059
1060
  FROM sessions s
1060
1061
  ORDER BY s.created_at DESC
@@ -1161,7 +1162,7 @@ def get_session_detail(session_id):
1161
1162
  signals = safe_rows(query_rows("""
1162
1163
  SELECT * FROM exchange_signals
1163
1164
  WHERE session_id = ?
1164
- ORDER BY created_at DESC
1165
+ ORDER BY ts DESC
1165
1166
  """, (session_id,))) if has_signals else []
1166
1167
 
1167
1168
  signal_summary = safe_rows(query_rows("""
@@ -1195,18 +1196,20 @@ def get_search_results(query, limit=50):
1195
1196
  """Full-text search across exchanges."""
1196
1197
  try:
1197
1198
  results = query_rows("""
1198
- SELECT DISTINCT
1199
- s.id as session_id,
1199
+ SELECT
1200
+ e.session_id,
1200
1201
  s.title,
1201
- s.heat_score,
1202
+ sa.heat_score,
1202
1203
  e.id as exchange_id,
1203
- e.user_input,
1204
- e.assistant_response,
1205
- RANK() OVER (ORDER BY rank) as relevance
1206
- FROM sessions s
1207
- JOIN exchanges e ON s.id = e.session_id
1208
- JOIN full_text_search fts ON e.id = fts.exchange_id
1209
- WHERE fts.full_text_search MATCH ?
1204
+ e.exchange_index,
1205
+ e.user_message,
1206
+ e.response_text,
1207
+ e.user_ts
1208
+ FROM fts_exchanges fts
1209
+ JOIN exchanges e ON fts.rowid = e.id
1210
+ JOIN sessions s ON e.session_id = s.session_id
1211
+ LEFT JOIN session_analysis sa ON sa.session_id = e.session_id
1212
+ WHERE fts_exchanges MATCH ?
1210
1213
  ORDER BY rank
1211
1214
  LIMIT ?
1212
1215
  """, (query, limit))
@@ -1219,13 +1222,11 @@ def get_workspaces():
1219
1222
  """List all workspaces with session counts."""
1220
1223
  try:
1221
1224
  workspaces = query_rows("""
1222
- SELECT DISTINCT workspace_id,
1223
- COUNT(*) as session_count,
1224
- MAX(created_at) as last_session
1225
- FROM sessions
1226
- WHERE workspace_id IS NOT NULL
1227
- GROUP BY workspace_id
1228
- ORDER BY session_count DESC
1225
+ SELECT w.workspace_id, w.uri, w.name, w.type, w.session_count,
1226
+ (SELECT MAX(s.created_at) FROM sessions s
1227
+ WHERE s.workspace_id = w.workspace_id) as last_session
1228
+ FROM workspaces w
1229
+ ORDER BY w.session_count DESC
1229
1230
  """)
1230
1231
  return {"workspaces": workspaces}
1231
1232
  except Exception as e:
@@ -1253,9 +1254,9 @@ def get_memory():
1253
1254
  """Get all memory files."""
1254
1255
  try:
1255
1256
  memory = query_rows("""
1256
- SELECT id, name, size, created_at, updated_at
1257
+ SELECT id, scope, workspace_id, session_id, filename, size_bytes, ingested_at
1257
1258
  FROM memory_files
1258
- ORDER BY updated_at DESC
1259
+ ORDER BY ingested_at DESC
1259
1260
  """)
1260
1261
  return {"memory": memory}
1261
1262
  except Exception as e:
@@ -1267,21 +1268,25 @@ def get_tool_calls(query_str=None, limit=50):
1267
1268
  try:
1268
1269
  if query_str:
1269
1270
  results = query_rows("""
1270
- SELECT tc.*, e.session_id, s.title as session_title
1271
+ SELECT tc.id, tc.session_id, tc.exchange_index, tc.request_id,
1272
+ tc.tool_call_id, tc.tool_name, tc.file_path,
1273
+ tc.arguments_json, tc.has_output, tc.ingested_at,
1274
+ s.title as session_title
1271
1275
  FROM tool_calls tc
1272
- JOIN exchanges e ON tc.exchange_id = e.id
1273
- JOIN sessions s ON e.session_id = s.id
1274
- WHERE tc.tool_name LIKE ? OR tc.arguments LIKE ?
1275
- ORDER BY tc.created_at DESC
1276
+ JOIN sessions s ON tc.session_id = s.session_id
1277
+ WHERE tc.tool_name LIKE ? OR tc.arguments_json LIKE ?
1278
+ ORDER BY tc.ingested_at DESC
1276
1279
  LIMIT ?
1277
1280
  """, (f"%{query_str}%", f"%{query_str}%", limit))
1278
1281
  else:
1279
1282
  results = query_rows("""
1280
- SELECT tc.*, e.session_id, s.title as session_title
1283
+ SELECT tc.id, tc.session_id, tc.exchange_index, tc.request_id,
1284
+ tc.tool_call_id, tc.tool_name, tc.file_path,
1285
+ tc.arguments_json, tc.has_output, tc.ingested_at,
1286
+ s.title as session_title
1281
1287
  FROM tool_calls tc
1282
- JOIN exchanges e ON tc.exchange_id = e.id
1283
- JOIN sessions s ON e.session_id = s.id
1284
- ORDER BY tc.created_at DESC
1288
+ JOIN sessions s ON tc.session_id = s.session_id
1289
+ ORDER BY tc.ingested_at DESC
1285
1290
  LIMIT ?
1286
1291
  """, (limit,))
1287
1292
  return {"tool_calls": results, "query": query_str, "count": len(results)}
@@ -1293,10 +1298,11 @@ def get_vfs(session_id):
1293
1298
  """List VFS files for a session."""
1294
1299
  try:
1295
1300
  vfs = query_rows("""
1296
- SELECT id, session_id, path, size, created_at
1301
+ SELECT id, session_id, source_type, source_path, filename,
1302
+ content_type, size_bytes, sha256, ingested_at
1297
1303
  FROM vfs
1298
1304
  WHERE session_id = ?
1299
- ORDER BY path
1305
+ ORDER BY filename
1300
1306
  """, (session_id,))
1301
1307
  return {"vfs": vfs, "session_id": session_id}
1302
1308
  except Exception as e:
@@ -1354,17 +1360,26 @@ def get_tokens(session_id=None):
1354
1360
  if session_id:
1355
1361
  tokens = query_rows("""
1356
1362
  SELECT
1357
- SUM(CAST(json_extract(metadata, '$.token_count') AS INTEGER)) as total_tokens,
1358
- COUNT(*) as exchange_count
1359
- FROM exchanges
1363
+ SUM(prompt_tokens) as total_prompt,
1364
+ SUM(completion_tokens) as total_completion,
1365
+ SUM(cached_tokens) as total_cached,
1366
+ SUM(prompt_tokens + completion_tokens) as total_tokens,
1367
+ COUNT(*) as turn_count,
1368
+ GROUP_CONCAT(DISTINCT model_id) as models
1369
+ FROM token_usage
1360
1370
  WHERE session_id = ?
1361
1371
  """, (session_id,))
1362
1372
  else:
1363
1373
  tokens = query_rows("""
1364
1374
  SELECT
1365
- SUM(CAST(json_extract(metadata, '$.token_count') AS INTEGER)) as total_tokens,
1366
- COUNT(*) as exchange_count
1367
- FROM exchanges
1375
+ SUM(prompt_tokens) as total_prompt,
1376
+ SUM(completion_tokens) as total_completion,
1377
+ SUM(cached_tokens) as total_cached,
1378
+ SUM(prompt_tokens + completion_tokens) as total_tokens,
1379
+ COUNT(*) as turn_count,
1380
+ COUNT(DISTINCT session_id) as session_count,
1381
+ GROUP_CONCAT(DISTINCT model_id) as models
1382
+ FROM token_usage
1368
1383
  """)
1369
1384
  return {"tokens": tokens}
1370
1385
  except Exception as e:
@@ -1726,11 +1741,10 @@ def render_tokens():
1726
1741
  return """
1727
1742
  <div class="page-header">
1728
1743
  <div class="page-title">Token Usage</div>
1729
- <div class="page-subtitle">Token consumption analysis by session.</div>
1730
- </div>
1731
- <div class="card">
1732
- <p>Token usage analysis coming soon.</p>
1744
+ <div class="page-subtitle">Token consumption across all sessions.</div>
1733
1745
  </div>
1746
+ <div id="tokens-summary" class="loading"><div class="spinner"></div>Loading...</div>
1747
+ <div id="tokens-table" style="margin-top:16px"></div>
1734
1748
  """
1735
1749
 
1736
1750
 
@@ -1910,6 +1924,9 @@ function initializePage(page) {
1910
1924
  case 'alerts':
1911
1925
  initAlerts();
1912
1926
  break;
1927
+ case 'tokens':
1928
+ initTokens();
1929
+ break;
1913
1930
  case 'pipeline':
1914
1931
  initPipeline();
1915
1932
  break;
@@ -2358,6 +2375,46 @@ function initKeywords() {
2358
2375
  });
2359
2376
  }
2360
2377
 
2378
+ function initTokens() {
2379
+ const summary = document.getElementById('tokens-summary');
2380
+ const table = document.getElementById('tokens-table');
2381
+ if (!summary) return;
2382
+ summary.innerHTML = '<div class="spinner"></div> Loading...';
2383
+ fetch('/api/tokens').then(r => r.json()).then(data => {
2384
+ const t = (data.tokens || [])[0] || {};
2385
+ const fmt = n => (n || 0).toLocaleString();
2386
+ summary.innerHTML = `
2387
+ <div class="grid-4">
2388
+ <div class="card"><div class="card-header">Total Tokens</div><div class="card-value">${fmt(t.total_tokens)}</div></div>
2389
+ <div class="card"><div class="card-header">Prompt</div><div class="card-value">${fmt(t.total_prompt)}</div></div>
2390
+ <div class="card"><div class="card-header">Completion</div><div class="card-value">${fmt(t.total_completion)}</div></div>
2391
+ <div class="card"><div class="card-header">Cached</div><div class="card-value">${fmt(t.total_cached)}</div></div>
2392
+ <div class="card"><div class="card-header">Sessions</div><div class="card-value">${fmt(t.session_count)}</div></div>
2393
+ <div class="card"><div class="card-header">Turns</div><div class="card-value">${fmt(t.turn_count)}</div></div>
2394
+ </div>
2395
+ <div class="card" style="margin-top:12px"><b>Models:</b> ${t.models || 'n/a'}</div>
2396
+ `;
2397
+ }).catch(() => {
2398
+ summary.innerHTML = '<div class="alert alert-danger">Failed to load token data.</div>';
2399
+ });
2400
+ if (table) {
2401
+ table.innerHTML = '<div class="spinner"></div> Loading sessions...';
2402
+ const sql = 'SELECT s.title, tu.session_id, SUM(tu.prompt_tokens) as prompt, SUM(tu.completion_tokens) as completion, SUM(tu.cached_tokens) as cached, SUM(tu.prompt_tokens + tu.completion_tokens) as total, COUNT(*) as turns, GROUP_CONCAT(DISTINCT tu.model_id) as models FROM token_usage tu JOIN sessions s ON tu.session_id = s.session_id GROUP BY tu.session_id ORDER BY total DESC LIMIT 50';
2403
+ fetch('/api/query', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({sql: sql})})
2404
+ .then(r => r.json()).then(data => {
2405
+ const rows = data.rows || [];
2406
+ if (!rows.length) { table.innerHTML = '<p>No per-session data.</p>'; return; }
2407
+ const fmt = n => (n || 0).toLocaleString();
2408
+ let html = '<div class="card"><div class="card-header">Top Sessions by Token Usage</div><table class="table"><thead><tr><th>Session</th><th>Total</th><th>Prompt</th><th>Completion</th><th>Cached</th><th>Turns</th><th>Models</th></tr></thead><tbody>';
2409
+ rows.forEach(r => {
2410
+ html += '<tr><td class="truncate">' + (r.title || r.session_id) + '</td><td>' + fmt(r.total) + '</td><td>' + fmt(r.prompt) + '</td><td>' + fmt(r.completion) + '</td><td>' + fmt(r.cached) + '</td><td>' + r.turns + '</td><td class="truncate">' + (r.models || '') + '</td></tr>';
2411
+ });
2412
+ html += '</tbody></table></div>';
2413
+ table.innerHTML = html;
2414
+ }).catch(() => { table.innerHTML = '<p>Failed to load session breakdown.</p>'; });
2415
+ }
2416
+ }
2417
+
2361
2418
  function initWorkspaces() {
2362
2419
  const container = document.getElementById('workspaces-content');
2363
2420
  if (!container) return;
@@ -2737,6 +2794,13 @@ def application(environ, start_response):
2737
2794
  start_response('200 OK', [('Content-Type', 'application/json')])
2738
2795
  return [response]
2739
2796
 
2797
+ elif path == '/api/tokens':
2798
+ session_id = query.get('session_id', [None])[0]
2799
+ data = get_tokens(session_id)
2800
+ response = json.dumps(data).encode('utf-8')
2801
+ start_response('200 OK', [('Content-Type', 'application/json')])
2802
+ return [response]
2803
+
2740
2804
  elif path == '/api/alerts':
2741
2805
  data = get_alerts()
2742
2806
  response = json.dumps(data).encode('utf-8')
@@ -5,6 +5,21 @@ 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.6] - 2026-05-11
9
+
10
+ ### Added
11
+ - **`~/Library/goCosmix/` home namespace** — all goCosmix apps share a unified `~/Library/goCosmix/` directory (macOS) or `~/.gocosmix/` (other). CDA now installs to `~/Library/goCosmix/apps/code-data-ark/` instead of `~/.cda/`. Provides a clean, organized home for all future goCosmix systems.
12
+ - **`GOCOSMIX_HOME`, `GOCOSMIX_APPS`, `GOCOSMIX_SYSTEM`** constants in `cda.kernel.paths` — shared namespace anchors for future goCosmix apps.
13
+ - **Auto PATH patching in `cda setup`** — detects where pip placed the `cda` binary and patches `~/.zprofile` if the bin dir is missing from PATH. Solves the macOS system Python `command not found` blocker.
14
+ - **`python3 -m cda` entry point** (`cda/__main__.py`) — bootstrapping fallback so users can run `python3 -m cda setup` before PATH is configured.
15
+ - **Legacy migration notice** — `cda setup` detects an existing `~/.cda/` and advises running `cda migrate-home`.
16
+
17
+ ### Changed
18
+ - `cda.kernel.paths.get_cda_home()` default changed from `~/.cda` to `~/Library/goCosmix/apps/code-data-ark` (macOS) / `~/.gocosmix/apps/code-data-ark` (other). `CDA_HOME` env var still overrides.
19
+ - `ensure_dirs()` now creates the full goCosmix namespace tree in addition to app-level dirs.
20
+ - README quickstart updated: install command is `python3 -m cda setup` as the safe default.
21
+ - `cda setup` Step 1 shows `+` for new dirs and `✓` for existing ones.
22
+
8
23
  ## [2.0.5] - 2026-05-11
9
24
 
10
25
  ### Added
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-data-ark"
7
- version = "2.0.5"
7
+ version = "2.0.7"
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"
@@ -52,10 +52,17 @@ The runtime is managed by an embedded process kernel (PMF) that supervises the w
52
52
  pip install code-data-ark
53
53
  ```
54
54
 
55
+ > **macOS / system Python note**: pip installs the `cda` binary to `~/Library/Python/3.x/bin/` which is not on `PATH` by default. Use the fallback below — `cda setup` will fix PATH for you automatically:
56
+ >
57
+ > ```bash
58
+ > python3 -m cda setup
59
+ > ```
60
+
55
61
  ### Install with pipx
56
62
 
57
63
  ```bash
58
64
  pipx install code-data-ark
65
+ # pipx automatically manages PATH — `cda setup` works immediately
59
66
  ```
60
67
 
61
68
  ### Install from source
@@ -74,25 +81,27 @@ pip install -e ".[dev]"
74
81
  make install-dev
75
82
  ```
76
83
 
77
- > The `cda` console command is installed into your active Python environment's `bin` directory. Activate your virtual environment before running `cda`.
84
+ > The `cda` console command is installed into your Python environment's `bin` directory. If it isn't on PATH yet, use `python3 -m cda setup` — setup patches `~/.zprofile` automatically.
78
85
 
79
86
  ## ⚡ Quick Start
80
87
 
81
88
  ```bash
82
89
  pip install code-data-ark
83
- cda setup
90
+ python3 -m cda setup # use this if `cda` isn't on PATH yet
84
91
  ```
85
92
 
86
- That's it. `cda setup` runs four steps in sequence:
93
+ After the first run, `cda setup` patches `~/.zprofile` so `cda` is on PATH in every new terminal.
94
+
95
+ `cda setup` runs four steps in sequence:
87
96
 
88
97
  | Step | What it does |
89
98
  |------|-------------|
90
- | **1. Init** | Creates `~/.cda/` directory tree, validates your VS Code data path |
91
- | **2. PMF install** | Registers a macOS LaunchAgent — CDA starts automatically on every login |
92
- | **3. Sync** | Ingests all VS Code + Copilot session data into `~/.cda/data/cda.db` |
99
+ | **1. Init** | Creates `~/Library/goCosmix/apps/code-data-ark/` all app data in one organized namespace. Also patches `~/.zprofile` if `cda` isn't on PATH yet. |
100
+ | **2. PMF install** | Registers a macOS LaunchAgent — CDA starts automatically on every login via `cda pmf up` |
101
+ | **3. Sync** | Ingests all VS Code + Copilot session data into `cda.db` |
93
102
  | **4. Up** | Starts the watcher daemon and web UI via the PMF kernel, opens browser |
94
103
 
95
- After setup, everything is managed by the **PMF kernel**. On every login, `launchd` calls `cda pmf up` which starts the watcher and web UI. No terminal interaction required.
104
+ All data lives in `~/Library/goCosmix/apps/code-data-ark/`. After setup, everything is managed by the **PMF kernel** no terminal interaction required.
96
105
 
97
106
  ### Options
98
107
 
@@ -0,0 +1 @@
1
+ 2.0.7
@@ -1 +0,0 @@
1
- 2.0.5
File without changes
File without changes
File without changes
File without changes