delimit-cli 4.0.5 → 4.0.6

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,105 @@
1
+ #!/bin/bash
2
+ # Delimit OS — the AI developer operating system
3
+ # Type 'delimit' to launch the TUI, or 'delimit <command>' for CLI tools
4
+ #
5
+ # Usage:
6
+ # delimit → Launch TUI (interactive terminal dashboard)
7
+ # delimit --quick → Quick status (non-interactive)
8
+ # delimit think → Trigger deliberation
9
+ # delimit build → Start autonomous build loop
10
+ # delimit ask <query> → Ask the swarm
11
+ # delimit lint <spec> → Lint an API spec
12
+ # delimit init → Initialize governance in current repo
13
+ # delimit setup → Configure AI assistants
14
+
15
+ set -e
16
+
17
+ DELIMIT_HOME="${DELIMIT_HOME:-$HOME/.delimit}"
18
+ GATEWAY="$DELIMIT_HOME/server/ai"
19
+
20
+ # If no args, launch TUI (interactive if terminal, quick if piped)
21
+ if [ $# -eq 0 ]; then
22
+ if [ -f "$GATEWAY/tui.py" ]; then
23
+ if [ -t 1 ] && [ -t 0 ]; then
24
+ cd "$DELIMIT_HOME/server" && exec python3 -m ai.tui
25
+ else
26
+ cd "$DELIMIT_HOME/server" && exec python3 -m ai.tui --quick
27
+ fi
28
+ else
29
+ # Fallback to npm CLI
30
+ exec delimit-cli "$@"
31
+ fi
32
+ fi
33
+
34
+ # Route commands
35
+ case "$1" in
36
+ --quick|-q)
37
+ if [ -f "$GATEWAY/tui.py" ]; then
38
+ cd "$DELIMIT_HOME/server" && exec python3 -m ai.tui --quick
39
+ else
40
+ exec delimit-cli status
41
+ fi
42
+ ;;
43
+ think|deliberate)
44
+ shift
45
+ QUESTION="${*:-What should we build next based on the current ledger and signals?}"
46
+ echo "[Delimit OS] Triggering deliberation..."
47
+ cd "$DELIMIT_HOME/server" && python3 -c "
48
+ from ai.deliberation import deliberate
49
+ import json
50
+ result = deliberate('''$QUESTION''', mode='dialogue', max_rounds=3)
51
+ if 'error' in result:
52
+ print(f'Error: {result[\"error\"]}')
53
+ elif result.get('mode') == 'single_model_reflection':
54
+ print(f'Model: {result.get(\"model\", \"?\")}')
55
+ print(f'\\nAdvocate:\\n{result.get(\"advocate\", \"\")[:500]}')
56
+ print(f'\\nCritic:\\n{result.get(\"critic\", \"\")[:500]}')
57
+ print(f'\\nSynthesis:\\n{result.get(\"synthesis\", \"\")}')
58
+ else:
59
+ print(f'Verdict: {result.get(\"final_verdict\", \"no consensus\")[:500]}')
60
+ print(f'Rounds: {result.get(\"rounds\", 0)}')
61
+ " 2>&1
62
+ ;;
63
+ build|loop)
64
+ shift
65
+ echo "[Delimit OS] Starting autonomous build loop..."
66
+ echo "Checking ledger for next task..."
67
+ cd "$DELIMIT_HOME/server" && python3 -c "
68
+ from ai.ledger_manager import get_context
69
+ import json
70
+ result = get_context()
71
+ items = result.get('next_up', [])
72
+ if items:
73
+ print(f'Next up: {items[0].get(\"id\", \"?\")} [{items[0].get(\"priority\", \"?\")}] {items[0].get(\"title\", \"?\")[:60]}')
74
+ print(f'Total open: {result.get(\"open_items\", 0)}')
75
+ else:
76
+ print('Ledger is clear — nothing to build.')
77
+ " 2>&1
78
+ ;;
79
+ ask)
80
+ shift
81
+ QUERY="$*"
82
+ if [ -z "$QUERY" ]; then
83
+ echo "Usage: delimit ask <question>"
84
+ exit 1
85
+ fi
86
+ echo "[Delimit OS] Checking context..."
87
+ cd "$DELIMIT_HOME/server" && python3 -c "
88
+ from ai.ledger_manager import get_context
89
+ import json
90
+ result = get_context()
91
+ print(json.dumps(result, indent=2)[:2000])
92
+ " 2>&1
93
+ ;;
94
+ status)
95
+ if [ -f "$GATEWAY/tui.py" ]; then
96
+ cd "$GATEWAY/.." && exec python3 -m ai.tui --quick
97
+ else
98
+ exec delimit-cli status
99
+ fi
100
+ ;;
101
+ *)
102
+ # Pass through to delimit-cli for all other commands
103
+ exec delimit-cli "$@"
104
+ ;;
105
+ esac
@@ -0,0 +1,296 @@
1
+ """Delimit TUI — Terminal User Interface (Phase 2 of Delimit OS).
2
+
3
+ The proprietary terminal experience. Type 'delimit' and get an OS-like
4
+ environment with panels for ledger, swarm, memory, and live logs.
5
+
6
+ Enterprise-ready: zero JS, pure Python, works over SSH, sub-2s boot.
7
+ Designed for devs who hate browser-based tools.
8
+
9
+ Usage:
10
+ python -m ai.tui # Full TUI
11
+ python -m ai.tui --quick # Quick status (no interactive mode)
12
+ """
13
+
14
+ from textual.app import App, ComposeResult
15
+ from textual.containers import Container, Horizontal, Vertical
16
+ from textual.widgets import (
17
+ Header, Footer, Static, DataTable, Log, TabbedContent, TabPane,
18
+ Label, ProgressBar, Button, Input,
19
+ )
20
+ from textual.timer import Timer
21
+ from textual import work
22
+ import json
23
+ import time
24
+ from pathlib import Path
25
+ from typing import Any, Dict, List
26
+
27
+
28
+ # ── Data loaders ─────────────────────────────────────────────────────
29
+
30
+ LEDGER_DIR = Path.home() / ".delimit" / "ledger"
31
+ SWARM_DIR = Path.home() / ".delimit" / "swarm"
32
+ MEMORY_DIR = Path.home() / ".delimit" / "memory"
33
+ SESSIONS_DIR = Path.home() / ".delimit" / "sessions"
34
+
35
+
36
+ def _load_ledger_items(status: str = "open", limit: int = 20) -> List[Dict]:
37
+ items = []
38
+ for fname in ("operations.jsonl", "strategy.jsonl"):
39
+ path = LEDGER_DIR / fname
40
+ if not path.exists():
41
+ continue
42
+ for line in path.read_text().strip().split("\n")[-200:]:
43
+ try:
44
+ d = json.loads(line)
45
+ if d.get("status") == status:
46
+ items.append(d)
47
+ except json.JSONDecodeError:
48
+ continue
49
+ items.sort(key=lambda x: (0 if x.get("priority") == "P0" else 1 if x.get("priority") == "P1" else 2))
50
+ return items[:limit]
51
+
52
+
53
+ def _load_swarm_status() -> Dict[str, Any]:
54
+ registry = SWARM_DIR / "agent_registry.json"
55
+ if not registry.exists():
56
+ return {"agents": 0, "ventures": 0}
57
+ try:
58
+ data = json.loads(registry.read_text())
59
+ agents = data.get("agents", {})
60
+ ventures = set(a.get("venture", "") for a in agents.values())
61
+ return {
62
+ "agents": len(agents),
63
+ "ventures": len(ventures),
64
+ "by_venture": {v: sum(1 for a in agents.values() if a.get("venture") == v) for v in ventures},
65
+ }
66
+ except (json.JSONDecodeError, KeyError):
67
+ return {"agents": 0, "ventures": 0}
68
+
69
+
70
+ def _load_recent_sessions(limit: int = 5) -> List[Dict]:
71
+ if not SESSIONS_DIR.exists():
72
+ return []
73
+ sessions = []
74
+ for f in sorted(SESSIONS_DIR.glob("*.json"), reverse=True)[:limit]:
75
+ try:
76
+ sessions.append(json.loads(f.read_text()))
77
+ except (json.JSONDecodeError, KeyError):
78
+ continue
79
+ return sessions
80
+
81
+
82
+ # ── Widgets ──────────────────────────────────────────────────────────
83
+
84
+ class LedgerPanel(Static):
85
+ """Live ledger view — shows open items sorted by priority."""
86
+
87
+ def compose(self) -> ComposeResult:
88
+ yield DataTable(id="ledger-table")
89
+
90
+ def on_mount(self) -> None:
91
+ table = self.query_one("#ledger-table", DataTable)
92
+ table.add_columns("ID", "P", "Title", "Venture", "Type")
93
+ self._refresh_data()
94
+ self.set_interval(30, self._refresh_data)
95
+
96
+ def _refresh_data(self) -> None:
97
+ table = self.query_one("#ledger-table", DataTable)
98
+ table.clear()
99
+ for item in _load_ledger_items("open", 25):
100
+ table.add_row(
101
+ item.get("id", ""),
102
+ item.get("priority", ""),
103
+ item.get("title", "")[:60],
104
+ item.get("venture", "")[:15],
105
+ item.get("type", ""),
106
+ )
107
+
108
+
109
+ class SwarmPanel(Static):
110
+ """Swarm status — agents, ventures, health."""
111
+
112
+ def compose(self) -> ComposeResult:
113
+ yield Static(id="swarm-content")
114
+
115
+ def on_mount(self) -> None:
116
+ self._refresh_data()
117
+ self.set_interval(15, self._refresh_data)
118
+
119
+ def _refresh_data(self) -> None:
120
+ content = self.query_one("#swarm-content", Static)
121
+ swarm = _load_swarm_status()
122
+ lines = [
123
+ f"[bold cyan]Agents:[/] {swarm['agents']} | [bold cyan]Ventures:[/] {swarm['ventures']}",
124
+ "",
125
+ ]
126
+ for venture, count in swarm.get("by_venture", {}).items():
127
+ lines.append(f" [green]{venture}[/]: {count} agents")
128
+ content.update("\n".join(lines))
129
+
130
+
131
+ class SessionPanel(Static):
132
+ """Recent sessions — handoff history."""
133
+
134
+ def compose(self) -> ComposeResult:
135
+ yield Static(id="session-content")
136
+
137
+ def on_mount(self) -> None:
138
+ self._refresh_data()
139
+
140
+ def _refresh_data(self) -> None:
141
+ content = self.query_one("#session-content", Static)
142
+ sessions = _load_recent_sessions(5)
143
+ if not sessions:
144
+ content.update("[dim]No sessions recorded yet.[/]")
145
+ return
146
+ lines = []
147
+ for s in sessions:
148
+ ts = s.get("timestamp", s.get("closed_at", ""))[:16]
149
+ summary = s.get("summary", "")[:80]
150
+ completed = len(s.get("items_completed", []))
151
+ lines.append(f"[dim]{ts}[/] — {summary}")
152
+ if completed:
153
+ lines.append(f" [green]✓ {completed} items completed[/]")
154
+ content.update("\n".join(lines))
155
+
156
+
157
+ class GovernanceBar(Static):
158
+ """Top status bar — governance health at a glance."""
159
+
160
+ def compose(self) -> ComposeResult:
161
+ yield Static(id="gov-bar")
162
+
163
+ def on_mount(self) -> None:
164
+ self._refresh()
165
+ self.set_interval(60, self._refresh)
166
+
167
+ def _refresh(self) -> None:
168
+ bar = self.query_one("#gov-bar", Static)
169
+ ledger_count = len(_load_ledger_items("open", 999))
170
+ swarm = _load_swarm_status()
171
+ mode_file = Path.home() / ".delimit" / "enforcement_mode"
172
+ mode = mode_file.read_text().strip() if mode_file.exists() else "default"
173
+
174
+ bar.update(
175
+ f" [bold magenta]</>[/] [bold]Delimit OS[/] | "
176
+ f"[cyan]Ledger:[/] {ledger_count} open | "
177
+ f"[cyan]Swarm:[/] {swarm['agents']} agents / {swarm['ventures']} ventures | "
178
+ f"[cyan]Mode:[/] {mode} | "
179
+ f"[dim]{time.strftime('%H:%M')}[/]"
180
+ )
181
+
182
+
183
+ # ── Main App ─────────────────────────────────────────────────────────
184
+
185
+ class DelimitOS(App):
186
+ """Delimit OS — the AI developer operating system."""
187
+
188
+ CSS = """
189
+ Screen {
190
+ background: $surface;
191
+ }
192
+ #gov-bar {
193
+ height: 1;
194
+ background: $primary-background;
195
+ color: $text;
196
+ padding: 0 1;
197
+ }
198
+ TabbedContent {
199
+ height: 1fr;
200
+ }
201
+ DataTable {
202
+ height: 1fr;
203
+ }
204
+ #swarm-content, #session-content {
205
+ padding: 1;
206
+ }
207
+ """
208
+
209
+ TITLE = "Delimit OS"
210
+ SUB_TITLE = "AI Developer Operating System"
211
+
212
+ BINDINGS = [
213
+ ("q", "quit", "Quit"),
214
+ ("l", "focus_ledger", "Ledger"),
215
+ ("s", "focus_swarm", "Swarm"),
216
+ ("h", "focus_sessions", "History"),
217
+ ("r", "refresh", "Refresh"),
218
+ ("t", "think", "Think"),
219
+ ("b", "build", "Build"),
220
+ ]
221
+
222
+ def compose(self) -> ComposeResult:
223
+ yield GovernanceBar()
224
+ with TabbedContent():
225
+ with TabPane("Ledger", id="tab-ledger"):
226
+ yield LedgerPanel()
227
+ with TabPane("Swarm", id="tab-swarm"):
228
+ yield SwarmPanel()
229
+ with TabPane("Sessions", id="tab-sessions"):
230
+ yield SessionPanel()
231
+ yield Footer()
232
+
233
+ def action_focus_ledger(self) -> None:
234
+ self.query_one(TabbedContent).active = "tab-ledger"
235
+
236
+ def action_focus_swarm(self) -> None:
237
+ self.query_one(TabbedContent).active = "tab-swarm"
238
+
239
+ def action_focus_sessions(self) -> None:
240
+ self.query_one(TabbedContent).active = "tab-sessions"
241
+
242
+ def action_refresh(self) -> None:
243
+ for panel in self.query(LedgerPanel):
244
+ panel._refresh_data()
245
+ for panel in self.query(SwarmPanel):
246
+ panel._refresh_data()
247
+ for panel in self.query(SessionPanel):
248
+ panel._refresh_data()
249
+ self.query_one(GovernanceBar)._refresh()
250
+
251
+ def action_think(self) -> None:
252
+ self.notify("Deliberation triggered — check ledger for results", title="Think")
253
+
254
+ def action_build(self) -> None:
255
+ self.notify("Build loop started — swarm dispatching tasks", title="Build")
256
+
257
+
258
+ def main():
259
+ """Entry point for 'delimit' command."""
260
+ import sys
261
+ if "--quick" in sys.argv:
262
+ # Quick status mode — no interactive TUI
263
+ from rich.console import Console
264
+ from rich.table import Table
265
+
266
+ console = Console()
267
+ console.print("\n[bold magenta]</>[/] [bold]Delimit OS[/]\n")
268
+
269
+ swarm = _load_swarm_status()
270
+ items = _load_ledger_items("open", 10)
271
+
272
+ console.print(f"[cyan]Swarm:[/] {swarm['agents']} agents across {swarm['ventures']} ventures")
273
+ console.print(f"[cyan]Ledger:[/] {len(items)} open items\n")
274
+
275
+ if items:
276
+ table = Table(title="Open Items")
277
+ table.add_column("ID", style="dim")
278
+ table.add_column("P", style="bold")
279
+ table.add_column("Title")
280
+ table.add_column("Venture", style="green")
281
+ for item in items[:10]:
282
+ table.add_row(
283
+ item.get("id", ""),
284
+ item.get("priority", ""),
285
+ item.get("title", "")[:60],
286
+ item.get("venture", "")[:15],
287
+ )
288
+ console.print(table)
289
+ return
290
+
291
+ app = DelimitOS()
292
+ app.run()
293
+
294
+
295
+ if __name__ == "__main__":
296
+ main()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.0.5",
4
+ "version": "4.0.6",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [