@0dai-dev/cli 4.3.5 → 4.3.7

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.
Files changed (79) hide show
  1. package/README.md +12 -11
  2. package/bin/0dai.js +214 -40
  3. package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
  4. package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
  5. package/lib/ai/registry/mcp-catalog.json +98 -0
  6. package/lib/commands/auth.js +55 -1
  7. package/lib/commands/compliance.js +1 -1
  8. package/lib/commands/detect.js +10 -4
  9. package/lib/commands/doctor.js +545 -26
  10. package/lib/commands/experience.js +40 -5
  11. package/lib/commands/export.js +73 -0
  12. package/lib/commands/feedback.js +157 -15
  13. package/lib/commands/gh.js +26 -0
  14. package/lib/commands/graph.js +9 -4
  15. package/lib/commands/heatmap.js +1 -1
  16. package/lib/commands/init.js +222 -30
  17. package/lib/commands/mcp.js +129 -21
  18. package/lib/commands/models.js +138 -41
  19. package/lib/commands/provider.js +30 -59
  20. package/lib/commands/quota.js +1 -1
  21. package/lib/commands/receipt.js +1 -1
  22. package/lib/commands/run.js +18 -7
  23. package/lib/commands/runner.js +31 -1
  24. package/lib/commands/status.js +44 -11
  25. package/lib/commands/swarm.js +130 -12
  26. package/lib/commands/trust.js +286 -0
  27. package/lib/commands/update.js +184 -38
  28. package/lib/commands/usage.js +1 -1
  29. package/lib/commands/validate.js +32 -3
  30. package/lib/commands/vault.js +46 -9
  31. package/lib/python/__init__.py +0 -0
  32. package/lib/python/agent_quotas.py +525 -0
  33. package/lib/python/anomaly_alert.py +397 -0
  34. package/lib/python/anti_pattern_detector.py +799 -0
  35. package/lib/python/auth.py +443 -0
  36. package/lib/python/capi_profile_guard.py +477 -0
  37. package/lib/python/compliance_report.py +581 -0
  38. package/lib/python/drift_detector.py +388 -0
  39. package/lib/python/experience_pipeline.py +1130 -0
  40. package/lib/python/graph.py +19 -0
  41. package/lib/python/graph_core.py +293 -0
  42. package/lib/python/graph_io.py +179 -0
  43. package/lib/python/graph_legacy.py +2052 -0
  44. package/lib/python/graph_legacy_helpers.py +221 -0
  45. package/lib/python/graph_outcomes_core.py +85 -0
  46. package/lib/python/graph_queries.py +171 -0
  47. package/lib/python/graph_slice.py +198 -0
  48. package/lib/python/graph_slicer.py +576 -0
  49. package/lib/python/graph_slicer_cli.py +60 -0
  50. package/lib/python/graph_validation.py +64 -0
  51. package/lib/python/heatmap.py +934 -0
  52. package/lib/python/json_utils.py +193 -0
  53. package/lib/python/mcp_exposure_check.py +247 -0
  54. package/lib/python/model_router.py +1434 -0
  55. package/lib/python/project_manager.py +621 -0
  56. package/lib/python/provider_profiles.py +1618 -0
  57. package/lib/python/provider_registry.py +1211 -0
  58. package/lib/python/provider_registry_cli.py +125 -0
  59. package/lib/python/receipt_png.py +727 -0
  60. package/lib/python/structural_memory.py +325 -0
  61. package/lib/python/swarm_cost.py +177 -0
  62. package/lib/python/usage_ledger.py +569 -0
  63. package/lib/scripts/mcp_tier_config.py +240 -0
  64. package/lib/shared.js +97 -14
  65. package/lib/tui/index.mjs +35174 -0
  66. package/lib/utils/activation_telemetry.js +230 -11
  67. package/lib/utils/constants.js +7 -1
  68. package/lib/utils/export-bundler.js +285 -0
  69. package/lib/utils/identity.js +198 -1
  70. package/lib/utils/mcp-auth.js +81 -15
  71. package/lib/utils/plan.js +1 -1
  72. package/lib/vault/index.js +19 -3
  73. package/lib/vault/storage.js +21 -2
  74. package/lib/wizard.js +5 -2
  75. package/package.json +9 -3
  76. package/scripts/build-python-bundle.js +106 -0
  77. package/scripts/build-tui.js +14 -1
  78. package/scripts/harvest_experience.py +523 -0
  79. package/scripts/postinstall.js +15 -9
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """Compatibility facade for the split graph modules."""
3
+
4
+ import graph_core as _graph_core
5
+ import graph_legacy as _graph_legacy
6
+
7
+ from graph_legacy import * # noqa: F403
8
+ from graph_core import * # noqa: F403
9
+ from graph_validation import * # noqa: F403
10
+ from graph_io import * # noqa: F403
11
+ from graph_slice import * # noqa: F403
12
+ from graph_queries import * # noqa: F403
13
+ from graph_outcomes_core import * # noqa: F403
14
+
15
+ _now_iso = _graph_core._now_iso
16
+ _slug = _graph_core._slug
17
+ _summary = _graph_legacy._summary
18
+ _parse_constraints_yaml = _graph_legacy._parse_constraints_yaml
19
+ _extract_violation_pattern = _graph_legacy._extract_violation_pattern
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env python3
2
+ """Core graph registries and mutation helpers."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import datetime as _dt
7
+ import logging
8
+ import re
9
+ from typing import Any, Optional
10
+
11
+ log = logging.getLogger("0dai.graph")
12
+
13
+ SCHEMA_VERSION = 1
14
+
15
+ NODE_TYPES = frozenset({
16
+ "Component",
17
+ "Technology",
18
+ "Decision",
19
+ "Requirement",
20
+ "Risk",
21
+ "TestPlan",
22
+ "Endpoint",
23
+ "DesignArtifact",
24
+ "MarketEntity",
25
+ "Session",
26
+ "Deliberation",
27
+ "Outcome",
28
+ "Artifact",
29
+ "Event",
30
+ "Constraint",
31
+ })
32
+
33
+ NODE_ID_PREFIXES: dict[str, str] = {
34
+ "Component": "comp",
35
+ "Technology": "tech",
36
+ "Decision": "dec",
37
+ "Requirement": "req",
38
+ "Risk": "risk",
39
+ "TestPlan": "test",
40
+ "Endpoint": "ep",
41
+ "DesignArtifact": "design",
42
+ "MarketEntity": "mkt",
43
+ "Session": "session",
44
+ "Deliberation": "delib",
45
+ "Outcome": "outcome",
46
+ "Artifact": "artifact",
47
+ "Event": "event",
48
+ "Constraint": "cstr",
49
+ }
50
+
51
+ EDGE_TYPES = frozenset({
52
+ "uses",
53
+ "depends_on",
54
+ "exposes",
55
+ "part_of",
56
+ "affects",
57
+ "chose",
58
+ "satisfies",
59
+ "supersedes",
60
+ "introduces",
61
+ "mitigates",
62
+ "decided_in",
63
+ "covers",
64
+ "tests",
65
+ "blocks",
66
+ "violates",
67
+ "designs",
68
+ "follows",
69
+ "targets",
70
+ "competes_with",
71
+ "owned_by",
72
+ "identified_by",
73
+ "created_by",
74
+ "approved_by",
75
+ "produced",
76
+ "updated",
77
+ "evaluates",
78
+ "decision_outcome",
79
+ "decision_ancestry",
80
+ "released_as",
81
+ "contains",
82
+ "triggered_by",
83
+ "observed_in",
84
+ "declared_by",
85
+ "implies",
86
+ "constrains",
87
+ "forbids",
88
+ })
89
+
90
+ OUTCOME_STATUSES = frozenset({
91
+ "confirmed",
92
+ "revised",
93
+ "reverted",
94
+ "partially_applied",
95
+ })
96
+
97
+ STAGE_OUTCOME_THRESHOLDS_DAYS: dict[str, int] = {
98
+ "idea": 14,
99
+ "mvp": 14,
100
+ "growth": 30,
101
+ "scale": 60,
102
+ }
103
+ DEFAULT_STAGE_THRESHOLD_DAYS = 30
104
+
105
+ ROLE_TYPE_INTERESTS: dict[str, frozenset[str]] = {
106
+ "cto": frozenset({"Decision", "Technology", "Risk", "Component", "Requirement", "Constraint"}),
107
+ "arch": frozenset({"Component", "Technology", "Endpoint", "Requirement", "Risk", "Constraint"}),
108
+ "designer": frozenset({"DesignArtifact", "Component", "Requirement"}),
109
+ "art_director": frozenset({"DesignArtifact", "Component", "MarketEntity"}),
110
+ "qa": frozenset({"TestPlan", "Component", "Risk", "Requirement", "Constraint"}),
111
+ "security": frozenset({"Risk", "Requirement", "Component", "Technology", "Constraint"}),
112
+ "sre": frozenset({"Component", "Technology", "Risk", "Endpoint", "Constraint"}),
113
+ "cmo": frozenset({"MarketEntity", "Requirement", "Component", "Decision"}),
114
+ }
115
+
116
+ DEFAULT_EDGE_WEIGHT = 1.0
117
+ HOP2_WEIGHT_THRESHOLD = 0.5
118
+
119
+ CHARS_PER_TOKEN = 4
120
+ DEFAULT_SLICE_TOKEN_BUDGET = 400
121
+
122
+ DETERMINISTIC_SOURCES = frozenset({
123
+ "bootstrap",
124
+ "file_parse",
125
+ "git_diff",
126
+ "operator",
127
+ "tool_output",
128
+ })
129
+
130
+ AI_DERIVED_SOURCES = frozenset({
131
+ "scout_ai",
132
+ "deliberation_ai",
133
+ "hard_block",
134
+ "red_team_ai",
135
+ "forecast_ai",
136
+ "pattern_ai",
137
+ })
138
+
139
+ DEFAULT_SOURCE = "operator"
140
+
141
+
142
+ def is_deterministic_source(source_type: str) -> bool:
143
+ return source_type in DETERMINISTIC_SOURCES
144
+
145
+
146
+ def source_marker(source_type: str) -> str:
147
+ if is_deterministic_source(source_type):
148
+ return "✓"
149
+ if source_type in AI_DERIVED_SOURCES:
150
+ return "~"
151
+ return "?"
152
+
153
+
154
+ def empty_graph() -> dict:
155
+ now = _now_iso()
156
+ return {
157
+ "nodes": {},
158
+ "edges": [],
159
+ "meta": {
160
+ "schema_version": SCHEMA_VERSION,
161
+ "created_at": now,
162
+ "updated_at": now,
163
+ "node_count": 0,
164
+ "edge_count": 0,
165
+ },
166
+ }
167
+
168
+
169
+ def _now_iso() -> str:
170
+ return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
171
+
172
+
173
+ def _slug(text: str) -> str:
174
+ cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", text.strip().lower())
175
+ return cleaned.strip("_") or "unnamed"
176
+
177
+
178
+ def make_node_id(node_type: str, name: str) -> str:
179
+ prefix = NODE_ID_PREFIXES.get(node_type)
180
+ if not prefix:
181
+ raise ValueError(f"unknown node type: {node_type}")
182
+ return f"{prefix}_{_slug(name)}"
183
+
184
+
185
+ def add_node(
186
+ graph: dict,
187
+ node_id: str,
188
+ node_type: str,
189
+ name: str,
190
+ *,
191
+ status: str = "active",
192
+ description: str = "",
193
+ source_type: str = DEFAULT_SOURCE,
194
+ extra: Optional[dict[str, Any]] = None,
195
+ ) -> dict:
196
+ if node_type not in NODE_TYPES:
197
+ raise ValueError(f"unknown node type: {node_type!r}")
198
+ if not node_id:
199
+ raise ValueError("node id must be non-empty")
200
+
201
+ now = _now_iso()
202
+ existing = graph["nodes"].get(node_id)
203
+ if existing:
204
+ existing["type"] = node_type
205
+ existing["name"] = name
206
+ existing["status"] = status
207
+ existing["description"] = description
208
+ existing["updated_at"] = now
209
+ existing_source = existing.get("source_type", DEFAULT_SOURCE)
210
+ if is_deterministic_source(source_type):
211
+ existing["source_type"] = source_type
212
+ elif not is_deterministic_source(existing_source):
213
+ existing["source_type"] = source_type
214
+ if extra:
215
+ existing.update(extra)
216
+ graph["meta"]["updated_at"] = now
217
+ return existing
218
+
219
+ node = {
220
+ "id": node_id,
221
+ "type": node_type,
222
+ "name": name,
223
+ "status": status,
224
+ "description": description,
225
+ "source_type": source_type,
226
+ "created_at": now,
227
+ "updated_at": now,
228
+ }
229
+ if extra:
230
+ node.update(extra)
231
+ graph["nodes"][node_id] = node
232
+ graph["meta"]["node_count"] = len(graph["nodes"])
233
+ graph["meta"]["updated_at"] = now
234
+ return node
235
+
236
+
237
+ def add_edge(
238
+ graph: dict,
239
+ source: str,
240
+ target: str,
241
+ edge_type: str,
242
+ *,
243
+ weight: float = DEFAULT_EDGE_WEIGHT,
244
+ extra: Optional[dict[str, Any]] = None,
245
+ ) -> dict:
246
+ if edge_type not in EDGE_TYPES:
247
+ raise ValueError(f"unknown edge type: {edge_type!r}")
248
+ if source not in graph["nodes"]:
249
+ raise ValueError(f"edge source {source!r} not in graph")
250
+ if target not in graph["nodes"]:
251
+ raise ValueError(f"edge target {target!r} not in graph")
252
+
253
+ edge = {
254
+ "from": source,
255
+ "to": target,
256
+ "type": edge_type,
257
+ "weight": float(weight),
258
+ "created_at": _now_iso(),
259
+ }
260
+ if extra:
261
+ edge.update(extra)
262
+ graph["edges"].append(edge)
263
+ graph["meta"]["edge_count"] = len(graph["edges"])
264
+ graph["meta"]["updated_at"] = edge["created_at"]
265
+ return edge
266
+
267
+
268
+ def find_edge(
269
+ graph: dict,
270
+ source: str,
271
+ target: str,
272
+ edge_type: Optional[str] = None,
273
+ ) -> Optional[dict]:
274
+ for edge in graph["edges"]:
275
+ if edge["from"] == source and edge["to"] == target:
276
+ if edge_type is None or edge["type"] == edge_type:
277
+ return edge
278
+ return None
279
+
280
+
281
+ def outgoing_edges(graph: dict, node_id: str) -> list[dict]:
282
+ return [e for e in graph["edges"] if e["from"] == node_id]
283
+
284
+
285
+ def incoming_edges(graph: dict, node_id: str) -> list[dict]:
286
+ return [e for e in graph["edges"] if e["to"] == node_id]
287
+
288
+
289
+ def nodes_by_type(graph: dict, node_type: str) -> list[dict]:
290
+ return sorted(
291
+ (n for n in graph["nodes"].values() if n.get("type") == node_type),
292
+ key=lambda n: n["id"],
293
+ )
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """Graph JSON I/O and usage tracking."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import pathlib
8
+
9
+ from graph_core import _now_iso, empty_graph, log
10
+ from graph_validation import GraphValidationError, validate_graph
11
+
12
+
13
+ def _usage_path(path: pathlib.Path) -> pathlib.Path:
14
+ path = pathlib.Path(path)
15
+ return path.with_name(f"{path.stem}_usage{path.suffix}")
16
+
17
+
18
+ def _default_usage() -> dict:
19
+ return {
20
+ "schema_version": 1,
21
+ "updated_at": _now_iso(),
22
+ "totals": {
23
+ "loads": 0,
24
+ "queries": 0,
25
+ "updates": 0,
26
+ "saves": 0,
27
+ },
28
+ "operations": {},
29
+ "recent": [],
30
+ }
31
+
32
+
33
+ def load_graph_usage(path: pathlib.Path) -> dict:
34
+ usage_path = _usage_path(path)
35
+ if not usage_path.exists():
36
+ return _default_usage()
37
+ try:
38
+ payload = json.loads(usage_path.read_text(encoding="utf-8"))
39
+ except (json.JSONDecodeError, OSError):
40
+ return _default_usage()
41
+
42
+ default = _default_usage()
43
+ if not isinstance(payload, dict):
44
+ return default
45
+
46
+ totals = payload.get("totals")
47
+ if not isinstance(totals, dict):
48
+ payload["totals"] = default["totals"]
49
+ else:
50
+ for key, value in default["totals"].items():
51
+ totals[key] = int(totals.get(key, value) or 0)
52
+
53
+ operations = payload.get("operations")
54
+ if not isinstance(operations, dict):
55
+ payload["operations"] = {}
56
+
57
+ recent = payload.get("recent")
58
+ if not isinstance(recent, list):
59
+ payload["recent"] = []
60
+
61
+ payload["schema_version"] = int(payload.get("schema_version", 1) or 1)
62
+ payload["updated_at"] = str(payload.get("updated_at") or default["updated_at"])
63
+ return payload
64
+
65
+
66
+ def save_graph_usage(path: pathlib.Path, usage: dict) -> None:
67
+ usage_path = _usage_path(path)
68
+ usage_path.parent.mkdir(parents=True, exist_ok=True)
69
+ usage_path.write_text(json.dumps(usage, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
70
+
71
+
72
+ def record_graph_usage(
73
+ graph: dict,
74
+ operation: str,
75
+ *,
76
+ kind: str = "queries",
77
+ count: int = 1,
78
+ ) -> None:
79
+ usage_path_raw = graph.get("_usage_path")
80
+ if not usage_path_raw:
81
+ return
82
+
83
+ try:
84
+ graph_path = pathlib.Path(str(usage_path_raw))
85
+ payload = load_graph_usage(graph_path)
86
+ except (TypeError, ValueError):
87
+ return
88
+
89
+ totals = payload.setdefault("totals", _default_usage()["totals"])
90
+ if kind in totals:
91
+ totals[kind] = int(totals.get(kind, 0) or 0) + count
92
+ operations = payload.setdefault("operations", {})
93
+ operations[operation] = int(operations.get(operation, 0) or 0) + count
94
+ payload["updated_at"] = _now_iso()
95
+ recent = payload.setdefault("recent", [])
96
+ recent.insert(0, {
97
+ "operation": operation,
98
+ "kind": kind,
99
+ "count": count,
100
+ "at": payload["updated_at"],
101
+ })
102
+ del recent[20:]
103
+
104
+ try:
105
+ save_graph_usage(graph_path, payload)
106
+ except OSError:
107
+ return
108
+
109
+
110
+ def summarize_graph_usage(path: pathlib.Path) -> dict:
111
+ usage = load_graph_usage(path)
112
+ operations = usage.get("operations", {})
113
+ if isinstance(operations, dict):
114
+ usage["top_operations"] = [
115
+ {"operation": name, "count": count}
116
+ for name, count in sorted(
117
+ ((str(name), int(count or 0)) for name, count in operations.items()),
118
+ key=lambda item: (-item[1], item[0]),
119
+ )[:8]
120
+ ]
121
+ else:
122
+ usage["top_operations"] = []
123
+ return usage
124
+
125
+
126
+ def load_graph(path: pathlib.Path, *, track_usage: bool = True) -> dict:
127
+ path = pathlib.Path(path)
128
+ if not path.exists():
129
+ log.info("graph file %s does not exist; returning empty graph", path)
130
+ graph = empty_graph()
131
+ if track_usage:
132
+ graph["_usage_path"] = str(path)
133
+ record_graph_usage(graph, "load_graph", kind="loads")
134
+ return graph
135
+
136
+ with path.open("r", encoding="utf-8") as f:
137
+ data = json.load(f)
138
+ if track_usage:
139
+ data["_usage_path"] = str(path)
140
+ record_graph_usage(data, "load_graph", kind="loads")
141
+ else:
142
+ data.pop("_usage_path", None)
143
+
144
+ errors = validate_graph(data)
145
+ if errors:
146
+ raise GraphValidationError(
147
+ f"graph at {path} failed validation:\n " + "\n ".join(errors)
148
+ )
149
+ return data
150
+
151
+
152
+ def save_graph(path: pathlib.Path, graph: dict, *, validate: bool = True) -> None:
153
+ path = pathlib.Path(path)
154
+ if validate:
155
+ errors = validate_graph(graph)
156
+ if errors:
157
+ raise GraphValidationError(
158
+ "refusing to save invalid graph:\n " + "\n ".join(errors)
159
+ )
160
+
161
+ graph["meta"]["updated_at"] = _now_iso()
162
+ graph["meta"]["node_count"] = len(graph["nodes"])
163
+ graph["meta"]["edge_count"] = len(graph["edges"])
164
+
165
+ stable = {
166
+ "meta": graph["meta"],
167
+ "nodes": dict(sorted(graph["nodes"].items())),
168
+ "edges": sorted(
169
+ graph["edges"],
170
+ key=lambda e: (e.get("from", ""), e.get("to", ""), e.get("type", "")),
171
+ ),
172
+ }
173
+
174
+ path.parent.mkdir(parents=True, exist_ok=True)
175
+ with path.open("w", encoding="utf-8") as f:
176
+ json.dump(stable, f, indent=2, ensure_ascii=False)
177
+ f.write("\n")
178
+ graph["_usage_path"] = str(path)
179
+ record_graph_usage(graph, "save_graph", kind="saves")