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.
- luna_intelligence-1.0.0/.gitignore +11 -0
- luna_intelligence-1.0.0/PKG-INFO +61 -0
- luna_intelligence-1.0.0/README.md +52 -0
- luna_intelligence-1.0.0/pyproject.toml +20 -0
- luna_intelligence-1.0.0/src/luna/__init__.py +7 -0
- luna_intelligence-1.0.0/src/luna/cli.py +71 -0
- luna_intelligence-1.0.0/src/luna/client.py +206 -0
- luna_intelligence-1.0.0/src/luna/models.py +45 -0
|
@@ -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,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)
|