squads-cli 0.4.10 → 0.4.13

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.
@@ -7,6 +7,7 @@ Forwards conversations to engram/mem0 for embeddings and graph storage.
7
7
  import os
8
8
  import json
9
9
  import gzip
10
+ import hashlib
10
11
  import threading
11
12
  import time
12
13
  import requests
@@ -22,7 +23,8 @@ app = Flask(__name__)
22
23
  # Configuration
23
24
  DEBUG_MODE = os.environ.get("DEBUG", "1") == "1"
24
25
  LANGFUSE_ENABLED = os.environ.get("LANGFUSE_ENABLED", "false").lower() == "true"
25
- DAILY_BUDGET = float(os.environ.get("SQUADS_DAILY_BUDGET", "200.0"))
26
+ # Monthly quota based on Anthropic plan (Max5 = $200/month)
27
+ MONTHLY_QUOTA = float(os.environ.get("SQUADS_MONTHLY_QUOTA", "200.0"))
26
28
  recent_logs = deque(maxlen=50)
27
29
 
28
30
  # Engram/mem0 configuration for memory extraction
@@ -128,8 +130,8 @@ def get_realtime_stats() -> dict:
128
130
  "output_tokens": output_tokens,
129
131
  "generations": generations,
130
132
  "by_squad": by_squad,
131
- "budget_remaining": DAILY_BUDGET - cost,
132
- "budget_pct": (cost / DAILY_BUDGET) * 100 if DAILY_BUDGET > 0 else 0,
133
+ "budget_remaining": MONTHLY_QUOTA - cost,
134
+ "budget_pct": (cost / MONTHLY_QUOTA) * 100 if MONTHLY_QUOTA > 0 else 0,
133
135
  }
134
136
  except Exception as e:
135
137
  print(f"Redis stats error: {e}")
@@ -763,7 +765,7 @@ def stats():
763
765
  "cost_usd": realtime["cost_usd"],
764
766
  },
765
767
  "budget": {
766
- "daily": DAILY_BUDGET,
768
+ "daily": MONTHLY_QUOTA,
767
769
  "used": realtime["cost_usd"],
768
770
  "remaining": realtime["budget_remaining"],
769
771
  "used_pct": realtime["budget_pct"],
@@ -820,10 +822,10 @@ def stats():
820
822
  "cost_usd": cost_usd,
821
823
  },
822
824
  "budget": {
823
- "daily": DAILY_BUDGET,
825
+ "daily": MONTHLY_QUOTA,
824
826
  "used": cost_usd,
825
- "remaining": DAILY_BUDGET - cost_usd,
826
- "used_pct": (cost_usd / DAILY_BUDGET) * 100 if DAILY_BUDGET > 0 else 0,
827
+ "remaining": MONTHLY_QUOTA - cost_usd,
828
+ "used_pct": (cost_usd / MONTHLY_QUOTA) * 100 if MONTHLY_QUOTA > 0 else 0,
827
829
  },
828
830
  "by_squad": [dict(r) for r in by_squad],
829
831
  "langfuse_enabled": langfuse is not None,
@@ -1757,6 +1759,530 @@ def brief_history():
1757
1759
  return jsonify({"error": str(e)}), 500
1758
1760
 
1759
1761
 
1762
+ # =============================================================================
1763
+ # Execution Gates API - Pre-execution safety checks
1764
+ # =============================================================================
1765
+
1766
+ @app.route("/api/execution/preflight", methods=["POST"])
1767
+ def preflight_check():
1768
+ """Check all execution gates before agent runs.
1769
+
1770
+ Returns:
1771
+ {
1772
+ "allowed": bool,
1773
+ "gates": {
1774
+ "budget": {"ok": bool, "used": float, "limit": float, "remaining": float},
1775
+ "cooldown": {"ok": bool, "elapsed_sec": int, "min_gap_sec": int}
1776
+ }
1777
+ }
1778
+ """
1779
+ try:
1780
+ data = request.get_json() or {}
1781
+ squad = data.get("squad", "hq")
1782
+ agent = data.get("agent")
1783
+ min_cooldown = int(data.get("min_cooldown_sec", 300)) # 5 min default
1784
+
1785
+ # Quota gate - check monthly spend against plan quota
1786
+ # Query monthly usage from Postgres (Redis only has daily stats)
1787
+ conn = get_db()
1788
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
1789
+ cur.execute("""
1790
+ SELECT COALESCE(SUM(cost_usd), 0) as cost_usd
1791
+ FROM squads.llm_generations
1792
+ WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)
1793
+ """)
1794
+ result = cur.fetchone()
1795
+ monthly_used = float(result["cost_usd"]) if result else 0
1796
+ conn.close()
1797
+
1798
+ quota_gate = {
1799
+ "ok": monthly_used < MONTHLY_QUOTA,
1800
+ "used": round(monthly_used, 2),
1801
+ "limit": MONTHLY_QUOTA,
1802
+ "remaining": round(MONTHLY_QUOTA - monthly_used, 2),
1803
+ "period": "month"
1804
+ }
1805
+
1806
+ # Cooldown gate (from existing tasks table)
1807
+ cooldown_gate = {"ok": True, "elapsed_sec": None, "min_gap_sec": min_cooldown}
1808
+
1809
+ if agent:
1810
+ conn = get_db()
1811
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
1812
+ cur.execute("""
1813
+ SELECT MAX(started_at) as last_run
1814
+ FROM squads.tasks
1815
+ WHERE squad = %s AND agent = %s
1816
+ """, (squad, agent))
1817
+ result = cur.fetchone()
1818
+ conn.close()
1819
+
1820
+ if result and result["last_run"]:
1821
+ last_run = result["last_run"]
1822
+ # Handle timezone-aware datetime
1823
+ now = datetime.now(last_run.tzinfo) if last_run.tzinfo else datetime.now()
1824
+ elapsed = (now - last_run).total_seconds()
1825
+ cooldown_gate = {
1826
+ "ok": elapsed >= min_cooldown,
1827
+ "elapsed_sec": int(elapsed),
1828
+ "min_gap_sec": min_cooldown,
1829
+ "last_run": last_run.isoformat()
1830
+ }
1831
+
1832
+ # Quota gate is informational only - never blocks execution
1833
+ # Track as KPI, not as enforcement (real limits come from Anthropic subscription)
1834
+ allowed = cooldown_gate["ok"]
1835
+
1836
+ if DEBUG_MODE:
1837
+ print(f"[PREFLIGHT] {squad}/{agent}: quota=${quota_gate['used']}/mo (KPI) cooldown={'OK' if cooldown_gate['ok'] else 'BLOCKED'}")
1838
+
1839
+ return jsonify({
1840
+ "allowed": allowed,
1841
+ "squad": squad,
1842
+ "agent": agent,
1843
+ "gates": {
1844
+ "quota": quota_gate,
1845
+ "cooldown": cooldown_gate
1846
+ }
1847
+ }), 200
1848
+
1849
+ except Exception as e:
1850
+ import traceback
1851
+ traceback.print_exc()
1852
+ # Fail open - if check fails, allow execution
1853
+ return jsonify({
1854
+ "allowed": True,
1855
+ "error": str(e),
1856
+ "gates": {}
1857
+ }), 200
1858
+
1859
+
1860
+ @app.route("/api/learnings/relevant", methods=["GET"])
1861
+ def get_relevant_learnings():
1862
+ """Get recent learnings for prompt injection.
1863
+
1864
+ Queries conversations marked as 'learning' type, ordered by importance.
1865
+
1866
+ Query params:
1867
+ squad: Filter by squad (optional)
1868
+ limit: Max results (default 5)
1869
+
1870
+ Returns:
1871
+ {
1872
+ "squad": str,
1873
+ "count": int,
1874
+ "learnings": [{"content": str, "importance": str, "created_at": str}]
1875
+ }
1876
+ """
1877
+ try:
1878
+ squad = request.args.get("squad")
1879
+ limit = min(int(request.args.get("limit", 5)), 20)
1880
+
1881
+ conn = get_db()
1882
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
1883
+ # Query conversations marked as learnings
1884
+ # Order by importance (high > normal > low), then recency
1885
+ if squad:
1886
+ cur.execute("""
1887
+ SELECT content, importance, created_at, squad
1888
+ FROM squads.conversations
1889
+ WHERE message_type = 'learning'
1890
+ AND (squad = %s OR squad IS NULL)
1891
+ ORDER BY
1892
+ CASE importance WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,
1893
+ created_at DESC
1894
+ LIMIT %s
1895
+ """, (squad, limit))
1896
+ else:
1897
+ cur.execute("""
1898
+ SELECT content, importance, created_at, squad
1899
+ FROM squads.conversations
1900
+ WHERE message_type = 'learning'
1901
+ ORDER BY
1902
+ CASE importance WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,
1903
+ created_at DESC
1904
+ LIMIT %s
1905
+ """, (limit,))
1906
+
1907
+ learnings = cur.fetchall()
1908
+ conn.close()
1909
+
1910
+ if DEBUG_MODE:
1911
+ print(f"[LEARNINGS] Found {len(learnings)} learnings for squad={squad}")
1912
+
1913
+ return jsonify({
1914
+ "squad": squad,
1915
+ "count": len(learnings),
1916
+ "learnings": [{
1917
+ "content": l["content"][:500] if l["content"] else "",
1918
+ "importance": l["importance"] or "normal",
1919
+ "squad": l["squad"],
1920
+ "created_at": l["created_at"].isoformat() if l["created_at"] else None
1921
+ } for l in learnings]
1922
+ }), 200
1923
+
1924
+ except Exception as e:
1925
+ import traceback
1926
+ traceback.print_exc()
1927
+ return jsonify({
1928
+ "squad": request.args.get("squad"),
1929
+ "count": 0,
1930
+ "learnings": [],
1931
+ "error": str(e)
1932
+ }), 200
1933
+
1934
+
1935
+ # =============================================================================
1936
+ # Dimension Sync API - Sync squad/agent definitions from CLI
1937
+ # =============================================================================
1938
+
1939
+ @app.route("/api/sync/dimensions", methods=["POST"])
1940
+ def sync_dimensions():
1941
+ """Sync squad and agent definitions to dimension tables.
1942
+
1943
+ Expects:
1944
+ {
1945
+ "squads": [{"name": str, "mission": str, "domain": str, ...}],
1946
+ "agents": [{"name": str, "squad": str, "role": str, ...}]
1947
+ }
1948
+
1949
+ Returns:
1950
+ {"synced_squads": int, "synced_agents": int}
1951
+ """
1952
+ try:
1953
+ data = request.get_json() or {}
1954
+ squads = data.get("squads", [])
1955
+ agents = data.get("agents", [])
1956
+
1957
+ conn = get_db()
1958
+ synced_squads = 0
1959
+ synced_agents = 0
1960
+
1961
+ with conn.cursor() as cur:
1962
+ # Upsert squads
1963
+ for squad in squads:
1964
+ cur.execute("""
1965
+ INSERT INTO squads.dim_squads (
1966
+ squad_name, mission, domain, default_provider,
1967
+ daily_budget, cooldown_seconds, metadata, updated_at
1968
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW())
1969
+ ON CONFLICT (squad_name) DO UPDATE SET
1970
+ mission = EXCLUDED.mission,
1971
+ domain = EXCLUDED.domain,
1972
+ default_provider = EXCLUDED.default_provider,
1973
+ daily_budget = EXCLUDED.daily_budget,
1974
+ cooldown_seconds = EXCLUDED.cooldown_seconds,
1975
+ metadata = EXCLUDED.metadata,
1976
+ updated_at = NOW()
1977
+ """, (
1978
+ squad["name"],
1979
+ squad.get("mission"),
1980
+ squad.get("domain"),
1981
+ squad.get("default_provider", "anthropic"),
1982
+ squad.get("daily_budget", 50),
1983
+ squad.get("cooldown_seconds", 300),
1984
+ json.dumps(squad.get("metadata", {})),
1985
+ ))
1986
+ synced_squads += 1
1987
+
1988
+ # Upsert agents (need squad_id)
1989
+ for agent in agents:
1990
+ # Get squad_id
1991
+ cur.execute(
1992
+ "SELECT id FROM squads.dim_squads WHERE squad_name = %s",
1993
+ (agent["squad"],)
1994
+ )
1995
+ row = cur.fetchone()
1996
+ squad_id = row[0] if row else None
1997
+
1998
+ if squad_id:
1999
+ cur.execute("""
2000
+ INSERT INTO squads.dim_agents (
2001
+ agent_name, squad_id, role, purpose, provider,
2002
+ trigger_type, mcp_servers, skills, metadata, updated_at
2003
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
2004
+ ON CONFLICT (agent_name, squad_id) DO UPDATE SET
2005
+ role = EXCLUDED.role,
2006
+ purpose = EXCLUDED.purpose,
2007
+ provider = EXCLUDED.provider,
2008
+ trigger_type = EXCLUDED.trigger_type,
2009
+ mcp_servers = EXCLUDED.mcp_servers,
2010
+ skills = EXCLUDED.skills,
2011
+ metadata = EXCLUDED.metadata,
2012
+ updated_at = NOW()
2013
+ """, (
2014
+ agent["name"],
2015
+ squad_id,
2016
+ agent.get("role"),
2017
+ agent.get("purpose"),
2018
+ agent.get("provider"),
2019
+ agent.get("trigger_type", "manual"),
2020
+ agent.get("mcp_servers", []),
2021
+ agent.get("skills", []),
2022
+ json.dumps(agent.get("metadata", {})),
2023
+ ))
2024
+ synced_agents += 1
2025
+
2026
+ conn.commit()
2027
+
2028
+ conn.close()
2029
+
2030
+ if DEBUG_MODE:
2031
+ print(f"[SYNC] Synced {synced_squads} squads, {synced_agents} agents")
2032
+
2033
+ return jsonify({
2034
+ "synced_squads": synced_squads,
2035
+ "synced_agents": synced_agents
2036
+ }), 200
2037
+
2038
+ except Exception as e:
2039
+ import traceback
2040
+ traceback.print_exc()
2041
+ return jsonify({"error": str(e)}), 500
2042
+
2043
+
2044
+ @app.route("/api/sync/learnings", methods=["POST"])
2045
+ def sync_learnings():
2046
+ """Sync learnings from CLI to Postgres conversations table.
2047
+
2048
+ Body:
2049
+ learnings: list of {squad, agent, content, category, importance, source_file}
2050
+
2051
+ Returns:
2052
+ {imported: int, skipped: int}
2053
+ """
2054
+ try:
2055
+ data = request.get_json() or {}
2056
+ learnings = data.get("learnings", [])
2057
+
2058
+ if not learnings:
2059
+ return jsonify({"imported": 0, "skipped": 0}), 200
2060
+
2061
+ conn = get_db()
2062
+ imported = 0
2063
+ skipped = 0
2064
+
2065
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
2066
+ for learning in learnings:
2067
+ # Check for duplicate (same content hash)
2068
+ content = learning.get("content", "")
2069
+ content_hash = hashlib.md5(content.encode()).hexdigest()
2070
+
2071
+ cur.execute("""
2072
+ SELECT id FROM squads.conversations
2073
+ WHERE message_type = 'learning'
2074
+ AND metadata->>'content_hash' = %s
2075
+ """, (content_hash,))
2076
+
2077
+ if cur.fetchone():
2078
+ skipped += 1
2079
+ continue
2080
+
2081
+ # Insert into conversations table as learning
2082
+ cur.execute("""
2083
+ INSERT INTO squads.conversations (
2084
+ role, content, message_type, importance, squad, agent, metadata
2085
+ ) VALUES (
2086
+ 'system', %s, 'learning', %s, %s, %s, %s
2087
+ )
2088
+ """, (
2089
+ content,
2090
+ learning.get("importance", "normal"),
2091
+ learning.get("squad"),
2092
+ learning.get("agent"),
2093
+ json.dumps({
2094
+ "category": learning.get("category", "insight"),
2095
+ "source_file": learning.get("source_file"),
2096
+ "content_hash": content_hash,
2097
+ "imported_at": datetime.now().isoformat(),
2098
+ }),
2099
+ ))
2100
+ imported += 1
2101
+
2102
+ conn.commit()
2103
+
2104
+ conn.close()
2105
+
2106
+ if DEBUG_MODE:
2107
+ print(f"[SYNC] Imported {imported} learnings, skipped {skipped} duplicates")
2108
+
2109
+ return jsonify({"imported": imported, "skipped": skipped}), 200
2110
+
2111
+ except Exception as e:
2112
+ import traceback
2113
+ traceback.print_exc()
2114
+ return jsonify({"error": str(e)}), 500
2115
+
2116
+
2117
+ # =============================================================================
2118
+ # Autonomy Score API - Calculate and return autonomy metrics
2119
+ # =============================================================================
2120
+
2121
+ @app.route("/api/autonomy/score", methods=["GET"])
2122
+ def get_autonomy_score():
2123
+ """Calculate current autonomy score.
2124
+
2125
+ Query params:
2126
+ squad: Filter by squad (optional)
2127
+ period: Time period - today, week, month (default: today)
2128
+
2129
+ Returns:
2130
+ {
2131
+ "overall_score": int (0-100),
2132
+ "confidence_level": str (low/medium/high),
2133
+ "components": {
2134
+ "budget_compliance": int,
2135
+ "cooldown_compliance": int,
2136
+ "quality_score": int,
2137
+ "success_rate": int,
2138
+ "learning_utilization": int
2139
+ },
2140
+ "execution_stats": {...}
2141
+ }
2142
+ """
2143
+ try:
2144
+ squad = request.args.get("squad")
2145
+ period = request.args.get("period", "today")
2146
+
2147
+ # Determine date range (tasks uses started_at, others use created_at)
2148
+ if period == "today":
2149
+ tasks_date_filter = "started_at >= CURRENT_DATE"
2150
+ date_filter = "created_at >= CURRENT_DATE"
2151
+ elif period == "week":
2152
+ tasks_date_filter = "started_at >= CURRENT_DATE - INTERVAL '7 days'"
2153
+ date_filter = "created_at >= CURRENT_DATE - INTERVAL '7 days'"
2154
+ elif period == "month":
2155
+ tasks_date_filter = "started_at >= CURRENT_DATE - INTERVAL '30 days'"
2156
+ date_filter = "created_at >= CURRENT_DATE - INTERVAL '30 days'"
2157
+ else:
2158
+ tasks_date_filter = "started_at >= CURRENT_DATE"
2159
+ date_filter = "created_at >= CURRENT_DATE"
2160
+
2161
+ conn = get_db()
2162
+ components = {}
2163
+
2164
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
2165
+ # Squad filter
2166
+ squad_filter = f"AND squad = '{squad}'" if squad else ""
2167
+
2168
+ # 1. Usage KPI (monthly spend tracking - informational, not a gate)
2169
+ cur.execute("""
2170
+ SELECT COALESCE(SUM(cost_usd), 0) as cost_usd
2171
+ FROM squads.llm_generations
2172
+ WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)
2173
+ """)
2174
+ monthly_result = cur.fetchone()
2175
+ monthly_used = float(monthly_result["cost_usd"]) if monthly_result else 0
2176
+ # KPI: Track usage but don't penalize - real limits come from Anthropic subscription
2177
+ # Score represents "how actively we're using our capacity" (higher = more active)
2178
+ components["usage_kpi"] = min(100, int(monthly_used / 10)) # $1000/mo = 100% utilization
2179
+
2180
+ # 2. Success rate from tasks
2181
+ cur.execute(f"""
2182
+ SELECT
2183
+ COUNT(*) as total,
2184
+ COUNT(*) FILTER (WHERE status = 'completed' AND success = true) as successful
2185
+ FROM squads.tasks
2186
+ WHERE {tasks_date_filter} {squad_filter}
2187
+ """)
2188
+ task_stats = cur.fetchone()
2189
+ total_tasks = task_stats["total"] or 0
2190
+ successful_tasks = task_stats["successful"] or 0
2191
+ components["success_rate"] = int(100 * successful_tasks / total_tasks) if total_tasks > 0 else 100
2192
+
2193
+ # 3. Quality score from feedback
2194
+ cur.execute(f"""
2195
+ SELECT AVG(quality_score) as avg_score, COUNT(*) as count
2196
+ FROM squads.task_feedback
2197
+ WHERE {date_filter}
2198
+ """)
2199
+ feedback_stats = cur.fetchone()
2200
+ avg_quality = feedback_stats["avg_score"] or 3.5 # Default to neutral
2201
+ components["quality_score"] = int(avg_quality * 20) # Scale 1-5 to 0-100
2202
+
2203
+ # 4. Cooldown compliance (check for rapid re-executions)
2204
+ cur.execute(f"""
2205
+ WITH exec_gaps AS (
2206
+ SELECT
2207
+ squad, agent,
2208
+ started_at,
2209
+ LAG(started_at) OVER (PARTITION BY squad, agent ORDER BY started_at) as prev_start
2210
+ FROM squads.tasks
2211
+ WHERE {tasks_date_filter} {squad_filter}
2212
+ )
2213
+ SELECT
2214
+ COUNT(*) as total,
2215
+ COUNT(*) FILTER (WHERE EXTRACT(EPOCH FROM (started_at - prev_start)) >= 300 OR prev_start IS NULL) as compliant
2216
+ FROM exec_gaps
2217
+ """)
2218
+ cooldown_stats = cur.fetchone()
2219
+ total_execs = cooldown_stats["total"] or 0
2220
+ compliant_execs = cooldown_stats["compliant"] or 0
2221
+ components["cooldown_compliance"] = int(100 * compliant_execs / total_execs) if total_execs > 0 else 100
2222
+
2223
+ # 5. Learning utilization (check if learnings exist in conversations)
2224
+ cur.execute("""
2225
+ SELECT COUNT(*) as learning_count
2226
+ FROM squads.conversations
2227
+ WHERE message_type = 'learning'
2228
+ AND created_at >= CURRENT_DATE - INTERVAL '30 days'
2229
+ """)
2230
+ learning_stats = cur.fetchone()
2231
+ learning_count = learning_stats["learning_count"] or 0
2232
+ # Score based on having learnings available (simple heuristic)
2233
+ components["learning_utilization"] = min(100, learning_count * 10) if learning_count > 0 else 0
2234
+
2235
+ conn.close()
2236
+
2237
+ # Calculate weighted overall score
2238
+ # Note: usage_kpi excluded from score calculation (it's informational only)
2239
+ weights = {
2240
+ "success_rate": 0.35,
2241
+ "quality_score": 0.30,
2242
+ "cooldown_compliance": 0.20,
2243
+ "learning_utilization": 0.15
2244
+ }
2245
+
2246
+ overall_score = int(sum(
2247
+ components.get(k, 0) * v for k, v in weights.items()
2248
+ ))
2249
+
2250
+ # Determine confidence level
2251
+ if overall_score >= 75:
2252
+ confidence_level = "high"
2253
+ elif overall_score >= 50:
2254
+ confidence_level = "medium"
2255
+ else:
2256
+ confidence_level = "low"
2257
+
2258
+ return jsonify({
2259
+ "overall_score": overall_score,
2260
+ "confidence_level": confidence_level,
2261
+ "period": period,
2262
+ "squad": squad,
2263
+ "components": components,
2264
+ "execution_stats": {
2265
+ "total_tasks": total_tasks,
2266
+ "successful_tasks": successful_tasks,
2267
+ "monthly_spend_usd": round(monthly_used, 2),
2268
+ "learning_count": learning_count
2269
+ },
2270
+ "usage_kpi": {
2271
+ "monthly_spend": round(monthly_used, 2),
2272
+ "note": "Track via /usage for real Anthropic limits"
2273
+ }
2274
+ }), 200
2275
+
2276
+ except Exception as e:
2277
+ import traceback
2278
+ traceback.print_exc()
2279
+ return jsonify({
2280
+ "overall_score": 50,
2281
+ "confidence_level": "unknown",
2282
+ "error": str(e)
2283
+ }), 200
2284
+
2285
+
1760
2286
  if __name__ == "__main__":
1761
2287
  port = int(os.environ.get("PORT", 8080))
1762
2288
  print(f"Starting Squads Bridge on port {port}")
@@ -1764,7 +2290,7 @@ if __name__ == "__main__":
1764
2290
  print(f" Redis: {'connected' if redis_client else 'disabled'}")
1765
2291
  print(f" Langfuse: {'enabled' if LANGFUSE_ENABLED else 'disabled'}")
1766
2292
  print(f" Engram: {'enabled -> ' + ENGRAM_URL if ENGRAM_ENABLED else 'disabled'}")
1767
- print(f" Budget: ${DAILY_BUDGET}/day")
2293
+ print(f" Usage KPI: tracking (real limits via /usage)")
1768
2294
 
1769
2295
  # Start background conversation processor
1770
2296
  if redis_client:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "squads-cli",
3
- "version": "0.4.10",
4
- "description": "A CLI for humans and agents",
3
+ "version": "0.4.13",
4
+ "description": "Open source CLI for AI agent coordination and execution. Domain-aligned squads with persistent memory and goals.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "squads": "./dist/cli.js"
@@ -39,13 +39,17 @@
39
39
  "prepublishOnly": "npm run build"
40
40
  },
41
41
  "keywords": [
42
- "ai",
43
- "agents",
44
- "squads",
45
- "cli",
46
- "automation",
47
42
  "claude",
48
- "llm"
43
+ "claude-code",
44
+ "anthropic",
45
+ "mcp",
46
+ "agent-sdk",
47
+ "multi-agent",
48
+ "orchestration",
49
+ "ai-agents",
50
+ "persistent-memory",
51
+ "squads",
52
+ "cli"
49
53
  ],
50
54
  "author": "Agents Squads <hello@agents-squads.com>",
51
55
  "license": "MIT",