luna-intelligence 1.0.0__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.
@@ -0,0 +1,11 @@
1
+ node_modules/
2
+ .next/
3
+ .env.local
4
+ .env.*.local
5
+ *.log
6
+ .DS_Store
7
+ .vscode/
8
+ .idea/
9
+ dist/
10
+ build/
11
+ .turbo/
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: luna-intelligence
3
+ Version: 1.0.0
4
+ Summary: LUNA — Developer activity intelligence. Monitor projects, enforce focus, generate AI session journals.
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: requests>=2.28.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # luna-intelligence
11
+
12
+ LUNA — Developer activity intelligence. Monitor projects, enforce focus, generate AI session journals.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install luna-intelligence
18
+ ```
19
+
20
+ ## Requirements
21
+
22
+ - Python 3.8+
23
+ - LUNA daemon running locally (default: `http://localhost:3030`)
24
+ - `luna` CLI on PATH
25
+
26
+ ## Usage
27
+
28
+ ### Python API
29
+
30
+ ```python
31
+ from luna import LunaClient
32
+
33
+ client = LunaClient()
34
+
35
+ # Query activity
36
+ events = client.events(limit=20)
37
+ stats = client.stats()
38
+
39
+ # Get directive
40
+ action = client.next_action()
41
+ print(action.type) # FOCUS | FINISH | EXECUTE
42
+ print(action.message)
43
+ print(action.directive) # list of imperative steps
44
+
45
+ # Set intent
46
+ client.set_intent("Finish LUNA v8 journal engine")
47
+
48
+ # Generate journal
49
+ path = client.journal() # triggers AI summary of last 6 hours
50
+ ```
51
+
52
+ ### CLI
53
+
54
+ ```bash
55
+ luna-py status
56
+ luna-py next
57
+ luna-py events --limit 50
58
+ luna-py stats
59
+ luna-py intent "Ship CADMUS to Isaac"
60
+ luna-py journal
61
+ ```
@@ -0,0 +1,52 @@
1
+ # luna-intelligence
2
+
3
+ LUNA — Developer activity intelligence. Monitor projects, enforce focus, generate AI session journals.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install luna-intelligence
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - Python 3.8+
14
+ - LUNA daemon running locally (default: `http://localhost:3030`)
15
+ - `luna` CLI on PATH
16
+
17
+ ## Usage
18
+
19
+ ### Python API
20
+
21
+ ```python
22
+ from luna import LunaClient
23
+
24
+ client = LunaClient()
25
+
26
+ # Query activity
27
+ events = client.events(limit=20)
28
+ stats = client.stats()
29
+
30
+ # Get directive
31
+ action = client.next_action()
32
+ print(action.type) # FOCUS | FINISH | EXECUTE
33
+ print(action.message)
34
+ print(action.directive) # list of imperative steps
35
+
36
+ # Set intent
37
+ client.set_intent("Finish LUNA v8 journal engine")
38
+
39
+ # Generate journal
40
+ path = client.journal() # triggers AI summary of last 6 hours
41
+ ```
42
+
43
+ ### CLI
44
+
45
+ ```bash
46
+ luna-py status
47
+ luna-py next
48
+ luna-py events --limit 50
49
+ luna-py stats
50
+ luna-py intent "Ship CADMUS to Isaac"
51
+ luna-py journal
52
+ ```
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "luna-intelligence"
7
+ version = "1.0.0"
8
+ description = "LUNA — Developer activity intelligence. Monitor projects, enforce focus, generate AI session journals."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ dependencies = [
13
+ "requests>=2.28.0",
14
+ ]
15
+
16
+ [project.scripts]
17
+ luna-py = "luna.cli:main"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/luna"]
@@ -0,0 +1,7 @@
1
+ """LUNA Intelligence — Python SDK"""
2
+
3
+ from .client import LunaClient
4
+ from .models import Event, Project, Stats, NextAction
5
+
6
+ __all__ = ["LunaClient", "Event", "Project", "Stats", "NextAction"]
7
+ __version__ = "1.0.0"
@@ -0,0 +1,71 @@
1
+ """luna-py CLI — thin wrapper exposing LUNA commands from Python."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from .client import LunaClient
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(
12
+ prog="luna-py",
13
+ description="LUNA Intelligence — Python CLI",
14
+ )
15
+ sub = parser.add_subparsers(dest="command")
16
+
17
+ sub.add_parser("status", help="Current project and intent")
18
+ sub.add_parser("next", help="Get LUNA directive (FOCUS / FINISH / EXECUTE)")
19
+ sub.add_parser("journal", help="Generate AI session journal")
20
+ sub.add_parser("stats", help="Per-project event counts (last hour)")
21
+
22
+ events_p = sub.add_parser("events", help="Recent activity events")
23
+ events_p.add_argument("--limit", type=int, default=20)
24
+
25
+ intent_p = sub.add_parser("intent", help="Set focus intent")
26
+ intent_p.add_argument("text", help="Intent text")
27
+
28
+ args = parser.parse_args()
29
+
30
+ if not args.command:
31
+ parser.print_help()
32
+ sys.exit(0)
33
+
34
+ client = LunaClient()
35
+
36
+ if args.command == "status":
37
+ s = client.status()
38
+ print(f"\nProject: {s.get('project', 'none')}")
39
+ print(f"Last Activity: {s.get('last_activity', 'none')}")
40
+ print(f"Intent: {s.get('intent', 'none')}\n")
41
+
42
+ elif args.command == "next":
43
+ action = client.next_action()
44
+ print(f"\n[{action.type}] {action.message}")
45
+ if action.directive:
46
+ print("\nDO THIS:")
47
+ for d in action.directive:
48
+ print(f" → {d}")
49
+ if action.suggestion:
50
+ print("\nContext:")
51
+ for s in action.suggestion:
52
+ print(f" - {s}")
53
+ print()
54
+
55
+ elif args.command == "journal":
56
+ path = client.journal()
57
+ print(f"Journal: {path}")
58
+
59
+ elif args.command == "stats":
60
+ stats = client.stats()
61
+ for s in stats:
62
+ print(f" {s.project:<40} {s.count} events")
63
+
64
+ elif args.command == "events":
65
+ events = client.events(limit=args.limit)
66
+ for e in events:
67
+ print(f" {e.time.strftime('%H:%M:%S')} {e.project:<30} {e.type}:{e.action} {e.target}")
68
+
69
+ elif args.command == "intent":
70
+ client.set_intent(args.text)
71
+ print(f"Intent set: {args.text}")
@@ -0,0 +1,206 @@
1
+ """
2
+ LunaClient — connects to the LUNA daemon API (default: http://localhost:3030).
3
+
4
+ LUNA must be running locally (managed by pm2). This client wraps the REST
5
+ endpoints exposed by LUNA's Express server and also exposes the CLI commands
6
+ via subprocess for journal generation and directives.
7
+
8
+ Usage:
9
+ from luna import LunaClient
10
+
11
+ client = LunaClient()
12
+ events = client.events(limit=10)
13
+ action = client.next_action()
14
+ client.log_event(project="my-repo", type="file", action="modified", target="src/main.py")
15
+ """
16
+
17
+ import subprocess
18
+ import json
19
+ from typing import List, Optional
20
+
21
+ try:
22
+ import requests
23
+ _has_requests = True
24
+ except ImportError:
25
+ _has_requests = False
26
+
27
+ from .models import Event, Project, Stats, NextAction
28
+
29
+
30
+ class LunaClient:
31
+ """
32
+ Python interface to a running LUNA daemon.
33
+
34
+ Args:
35
+ host: LUNA API host (default: localhost)
36
+ port: LUNA API port (default: 3030)
37
+ luna_bin: Path to the `luna` CLI binary (default: "luna" on PATH)
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ host: str = "localhost",
43
+ port: int = 3030,
44
+ luna_bin: str = "luna",
45
+ ):
46
+ self.base_url = f"http://{host}:{port}"
47
+ self.luna_bin = luna_bin
48
+
49
+ if not _has_requests:
50
+ raise ImportError(
51
+ "The 'requests' package is required. Install it with: pip install requests"
52
+ )
53
+
54
+ # ── REST API ──────────────────────────────────────────────────────────────
55
+
56
+ def events(self, limit: int = 50) -> List[Event]:
57
+ """Return the most recent activity events logged by LUNA."""
58
+ resp = requests.get(f"{self.base_url}/events", params={"limit": limit})
59
+ resp.raise_for_status()
60
+ return [Event(**e) for e in resp.json()]
61
+
62
+ def stats(self) -> List[Stats]:
63
+ """Return per-project event counts for the last hour."""
64
+ resp = requests.get(f"{self.base_url}/stats")
65
+ resp.raise_for_status()
66
+ return [Stats(**s) for s in resp.json()]
67
+
68
+ def log_event(
69
+ self,
70
+ project: str,
71
+ type: str,
72
+ action: str,
73
+ target: str,
74
+ metadata: Optional[dict] = None,
75
+ ) -> None:
76
+ """Push a custom event into LUNA's activity log."""
77
+ payload = {
78
+ "project": project,
79
+ "type": type,
80
+ "action": action,
81
+ "target": target,
82
+ "timestamp": _now_ms(),
83
+ "metadata": json.dumps(metadata) if metadata else None,
84
+ }
85
+ resp = requests.post(f"{self.base_url}/event", json=payload)
86
+ resp.raise_for_status()
87
+
88
+ # ── CLI commands (subprocess) ─────────────────────────────────────────────
89
+
90
+ def next_action(self) -> NextAction:
91
+ """
92
+ Get LUNA's current directive: FOCUS / FINISH / EXECUTE.
93
+ Parses the luna CLI output — returns a structured NextAction.
94
+ """
95
+ raw = self._run_cli("next")
96
+ return _parse_next_action(raw)
97
+
98
+ def status(self) -> dict:
99
+ """Return current project, last activity, and intent as a dict."""
100
+ raw = self._run_cli("status")
101
+ return _parse_status(raw)
102
+
103
+ def journal(self) -> str:
104
+ """
105
+ Trigger AI journal generation for the last 6 hours of activity.
106
+ Returns the path to the generated markdown file.
107
+ """
108
+ raw = self._run_cli("journal")
109
+ for line in raw.splitlines():
110
+ if "Journal created:" in line:
111
+ return line.split("Journal created:")[-1].strip()
112
+ return raw.strip()
113
+
114
+ def set_intent(self, text: str) -> None:
115
+ """Set the current focus intent."""
116
+ self._run_cli("intent", text)
117
+
118
+ def projects(self) -> List[dict]:
119
+ """Return raw project rows from LUNA's database."""
120
+ raw = self._run_cli_json("projects")
121
+ return raw if isinstance(raw, list) else []
122
+
123
+ # ── Internal ──────────────────────────────────────────────────────────────
124
+
125
+ def _run_cli(self, *args: str) -> str:
126
+ result = subprocess.run(
127
+ [self.luna_bin, *args],
128
+ capture_output=True,
129
+ text=True,
130
+ )
131
+ if result.returncode != 0 and result.stderr:
132
+ raise RuntimeError(f"LUNA CLI error: {result.stderr.strip()}")
133
+ return result.stdout
134
+
135
+ def _run_cli_json(self, *args: str) -> object:
136
+ """Run CLI command and attempt JSON parse of output."""
137
+ raw = self._run_cli(*args)
138
+ try:
139
+ return json.loads(raw)
140
+ except json.JSONDecodeError:
141
+ return raw
142
+
143
+
144
+ # ── Parsers ───────────────────────────────────────────────────────────────────
145
+
146
+ def _parse_next_action(raw: str) -> NextAction:
147
+ lines = [l.strip() for l in raw.splitlines() if l.strip()]
148
+ action_type = "EXECUTE"
149
+ message_lines = []
150
+ directives = []
151
+ suggestions = []
152
+ in_directive = False
153
+ in_suggestion = False
154
+
155
+ for line in lines:
156
+ if line.startswith("→"):
157
+ directives.append(line.lstrip("→").strip())
158
+ in_directive = True
159
+ in_suggestion = False
160
+ elif line.startswith("-"):
161
+ suggestions.append(line.lstrip("-").strip())
162
+ in_suggestion = True
163
+ in_directive = False
164
+ elif line in ("DO THIS:", "Context:"):
165
+ in_directive = line == "DO THIS:"
166
+ in_suggestion = line == "Context:"
167
+ elif "FOCUS" in line:
168
+ action_type = "FOCUS"
169
+ elif "FINISH" in line:
170
+ action_type = "FINISH"
171
+ elif not in_directive and not in_suggestion and line not in ("🧠 LUNA DIRECTIVE",):
172
+ message_lines.append(line)
173
+
174
+ # Infer type from message if not caught above
175
+ message = " ".join(message_lines).strip()
176
+ if "split across" in message:
177
+ action_type = "FOCUS"
178
+ elif "unfinished" in message.lower():
179
+ action_type = "FINISH"
180
+ elif "focused" in message.lower():
181
+ action_type = "EXECUTE"
182
+
183
+ return NextAction(
184
+ type=action_type,
185
+ message=message,
186
+ directive=directives,
187
+ suggestion=suggestions,
188
+ )
189
+
190
+
191
+ def _parse_status(raw: str) -> dict:
192
+ result = {}
193
+ for line in raw.splitlines():
194
+ line = line.strip()
195
+ if line.startswith("Project:"):
196
+ result["project"] = line.split(":", 1)[1].strip()
197
+ elif line.startswith("Last Activity:"):
198
+ result["last_activity"] = line.split(":", 1)[1].strip()
199
+ elif line.startswith("Intent:"):
200
+ result["intent"] = line.split(":", 1)[1].strip()
201
+ return result
202
+
203
+
204
+ def _now_ms() -> int:
205
+ import time
206
+ return int(time.time() * 1000)
@@ -0,0 +1,45 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime
3
+ from typing import List, Optional
4
+
5
+
6
+ @dataclass
7
+ class Event:
8
+ id: int
9
+ type: str
10
+ action: str
11
+ target: str
12
+ project: str
13
+ timestamp: int
14
+ metadata: Optional[str] = None
15
+
16
+ @property
17
+ def time(self) -> datetime:
18
+ return datetime.fromtimestamp(self.timestamp / 1000)
19
+
20
+
21
+ @dataclass
22
+ class Project:
23
+ id: int
24
+ name: str
25
+ path: str
26
+ status: str # active | stale | archive
27
+ last_active: int
28
+
29
+ @property
30
+ def last_active_time(self) -> datetime:
31
+ return datetime.fromtimestamp(self.last_active / 1000)
32
+
33
+
34
+ @dataclass
35
+ class Stats:
36
+ project: str
37
+ count: int
38
+
39
+
40
+ @dataclass
41
+ class NextAction:
42
+ type: str # FOCUS | FINISH | EXECUTE
43
+ message: str
44
+ directive: List[str] = field(default_factory=list)
45
+ suggestion: List[str] = field(default_factory=list)