delimit-cli 3.3.0 → 3.5.0

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,207 @@
1
+ """
2
+ Delimit Ledger Manager — Strategy + Operational ledger as first-class MCP tools.
3
+
4
+ Two ledgers:
5
+ - Strategy: consensus decisions, positioning, pricing, product direction
6
+ - Operational: tasks, bugs, features — the "keep building" items
7
+
8
+ Both are append-only JSONL at ~/.delimit/ledger/
9
+ """
10
+
11
+ import json
12
+ import hashlib
13
+ import time
14
+ import uuid
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ LEDGER_DIR = Path.home() / ".delimit" / "ledger"
19
+ STRATEGY_LEDGER = LEDGER_DIR / "strategy.jsonl"
20
+ OPS_LEDGER = LEDGER_DIR / "operations.jsonl"
21
+
22
+
23
+ def _ensure():
24
+ LEDGER_DIR.mkdir(parents=True, exist_ok=True)
25
+ for f in [STRATEGY_LEDGER, OPS_LEDGER]:
26
+ if not f.exists():
27
+ f.write_text("")
28
+
29
+
30
+ def _read_ledger(path: Path) -> List[Dict]:
31
+ _ensure()
32
+ items = []
33
+ for line in path.read_text().splitlines():
34
+ line = line.strip()
35
+ if line:
36
+ try:
37
+ items.append(json.loads(line))
38
+ except json.JSONDecodeError:
39
+ continue
40
+ return items
41
+
42
+
43
+ def _append(path: Path, entry: Dict) -> Dict:
44
+ _ensure()
45
+ # Add hash chain
46
+ items = _read_ledger(path)
47
+ prev_hash = items[-1].get("hash", "genesis") if items else "genesis"
48
+ entry["hash"] = hashlib.sha256(f"{prev_hash}{json.dumps(entry, sort_keys=True)}".encode()).hexdigest()[:16]
49
+ entry["created_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ")
50
+
51
+ with open(path, "a") as f:
52
+ f.write(json.dumps(entry) + "\n")
53
+ return entry
54
+
55
+
56
+ def add_item(
57
+ title: str,
58
+ ledger: str = "ops",
59
+ type: str = "task",
60
+ priority: str = "P1",
61
+ description: str = "",
62
+ source: str = "session",
63
+ tags: Optional[List[str]] = None,
64
+ ) -> Dict[str, Any]:
65
+ """Add a new item to the strategy or operational ledger."""
66
+ path = STRATEGY_LEDGER if ledger == "strategy" else OPS_LEDGER
67
+
68
+ # Auto-generate ID
69
+ items = _read_ledger(path)
70
+ prefix = "STR" if ledger == "strategy" else "LED"
71
+ existing_ids = [i.get("id", "") for i in items]
72
+ num = len(existing_ids) + 1
73
+ # Find next available number
74
+ while f"{prefix}-{num:03d}" in existing_ids:
75
+ num += 1
76
+ item_id = f"{prefix}-{num:03d}"
77
+
78
+ entry = {
79
+ "id": item_id,
80
+ "title": title,
81
+ "type": type,
82
+ "priority": priority,
83
+ "description": description,
84
+ "source": source,
85
+ "status": "open",
86
+ "tags": tags or [],
87
+ }
88
+
89
+ result = _append(path, entry)
90
+ return {
91
+ "added": result,
92
+ "ledger": ledger,
93
+ "total_items": len(_read_ledger(path)),
94
+ }
95
+
96
+
97
+ def update_item(
98
+ item_id: str,
99
+ status: Optional[str] = None,
100
+ note: Optional[str] = None,
101
+ priority: Optional[str] = None,
102
+ ) -> Dict[str, Any]:
103
+ """Update an existing ledger item's status, priority, or add a note."""
104
+ # Search both ledgers
105
+ for ledger_name, path in [("ops", OPS_LEDGER), ("strategy", STRATEGY_LEDGER)]:
106
+ items = _read_ledger(path)
107
+ for item in items:
108
+ if item.get("id") == item_id:
109
+ # Append an update event
110
+ update = {
111
+ "id": item_id,
112
+ "type": "update",
113
+ "updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
114
+ }
115
+ if status:
116
+ update["status"] = status
117
+ if note:
118
+ update["note"] = note
119
+ if priority:
120
+ update["priority"] = priority
121
+ _append(path, update)
122
+ return {"updated": item_id, "changes": update, "ledger": ledger_name}
123
+
124
+ return {"error": f"Item {item_id} not found in either ledger"}
125
+
126
+
127
+ def list_items(
128
+ ledger: str = "both",
129
+ status: Optional[str] = None,
130
+ priority: Optional[str] = None,
131
+ limit: int = 50,
132
+ ) -> Dict[str, Any]:
133
+ """List ledger items with optional filters."""
134
+ results = {}
135
+
136
+ for ledger_name, path in [("ops", OPS_LEDGER), ("strategy", STRATEGY_LEDGER)]:
137
+ if ledger not in ("both", ledger_name):
138
+ continue
139
+
140
+ items = _read_ledger(path)
141
+
142
+ # Build current state by replaying events
143
+ state = {}
144
+ for item in items:
145
+ item_id = item.get("id", "")
146
+ if item.get("type") == "update":
147
+ if item_id in state:
148
+ if "status" in item:
149
+ state[item_id]["status"] = item["status"]
150
+ if "note" in item:
151
+ state[item_id]["last_note"] = item["note"]
152
+ if "priority" in item:
153
+ state[item_id]["priority"] = item["priority"]
154
+ state[item_id]["updated_at"] = item.get("updated_at")
155
+ else:
156
+ state[item_id] = {**item}
157
+
158
+ # Filter
159
+ filtered = list(state.values())
160
+ if status:
161
+ filtered = [i for i in filtered if i.get("status") == status]
162
+ if priority:
163
+ filtered = [i for i in filtered if i.get("priority") == priority]
164
+
165
+ # Sort by priority then created_at
166
+ priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
167
+ filtered.sort(key=lambda x: (priority_order.get(x.get("priority", "P2"), 9), x.get("created_at", "")))
168
+
169
+ results[ledger_name] = filtered[:limit]
170
+
171
+ # Summary
172
+ all_items = []
173
+ for v in results.values():
174
+ all_items.extend(v)
175
+
176
+ open_count = sum(1 for i in all_items if i.get("status") == "open")
177
+ done_count = sum(1 for i in all_items if i.get("status") == "done")
178
+
179
+ return {
180
+ "items": results,
181
+ "summary": {
182
+ "total": len(all_items),
183
+ "open": open_count,
184
+ "done": done_count,
185
+ "in_progress": sum(1 for i in all_items if i.get("status") == "in_progress"),
186
+ },
187
+ }
188
+
189
+
190
+ def get_context() -> Dict[str, Any]:
191
+ """Get a concise ledger summary for AI context — what's open, what's next."""
192
+ result = list_items(status="open")
193
+ open_items = []
194
+ for ledger_items in result["items"].values():
195
+ open_items.extend(ledger_items)
196
+
197
+ # Sort by priority
198
+ priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
199
+ open_items.sort(key=lambda x: priority_order.get(x.get("priority", "P2"), 9))
200
+
201
+ return {
202
+ "open_items": len(open_items),
203
+ "next_up": [{"id": i["id"], "title": i["title"], "priority": i["priority"]}
204
+ for i in open_items[:5]],
205
+ "summary": result["summary"],
206
+ "tip": "Use delimit_ledger_add to add new items, delimit_ledger_done to mark complete.",
207
+ }