execweave 0.6.0__py3-none-any.whl
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.
- execweave/__init__.py +3 -0
- execweave/__main__.py +5 -0
- execweave/analysis.py +406 -0
- execweave/backends.py +63 -0
- execweave/benchmark.py +80 -0
- execweave/claude_adapter.py +448 -0
- execweave/claude_hook_cli.py +101 -0
- execweave/claude_record.py +106 -0
- execweave/cli.py +588 -0
- execweave/codex_adapter.py +314 -0
- execweave/codex_hook_cli.py +98 -0
- execweave/codex_record.py +111 -0
- execweave/collector.py +301 -0
- execweave/correlation.py +604 -0
- execweave/cursor_adapter.py +347 -0
- execweave/cursor_hook_cli.py +82 -0
- execweave/cursor_record.py +96 -0
- execweave/filesystem.py +103 -0
- execweave/focus.py +118 -0
- execweave/gemini_adapter.py +265 -0
- execweave/gemini_hook_cli.py +77 -0
- execweave/gemini_record.py +94 -0
- execweave/graph.py +300 -0
- execweave/graph_ops.py +446 -0
- execweave/inference_gateway.py +422 -0
- execweave/inference_gateway_cli.py +106 -0
- execweave/inference_identity.py +76 -0
- execweave/inference_identity_cli.py +60 -0
- execweave/live.py +275 -0
- execweave/model_runtime.py +535 -0
- execweave/model_runtime_cli.py +154 -0
- execweave/opencode_adapter.py +316 -0
- execweave/opencode_hook_cli.py +57 -0
- execweave/opencode_plugin_cli.py +110 -0
- execweave/opencode_record.py +96 -0
- execweave/overhead_benchmark.py +440 -0
- execweave/provider_record.py +215 -0
- execweave/schema.py +62 -0
- execweave/semantic.py +346 -0
- execweave/sink.py +33 -0
- execweave/strace_backend.py +682 -0
- execweave/validate.py +193 -0
- execweave/viewer.py +283 -0
- execweave/workflow.py +114 -0
- execweave-0.6.0.dist-info/METADATA +356 -0
- execweave-0.6.0.dist-info/RECORD +49 -0
- execweave-0.6.0.dist-info/WHEEL +4 -0
- execweave-0.6.0.dist-info/entry_points.txt +17 -0
- execweave-0.6.0.dist-info/licenses/LICENSE +21 -0
execweave/cli.py
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from .analysis import analyze_graph
|
|
9
|
+
from .backends import backend_diagnostics, create_collector, resolve_backend
|
|
10
|
+
from .benchmark import format_benchmark, run_benchmark
|
|
11
|
+
from .correlation import correlate_tool_process
|
|
12
|
+
from .focus import focus_graph
|
|
13
|
+
from .graph import build_execution_graph, write_execution_graph
|
|
14
|
+
from .graph_ops import (
|
|
15
|
+
condense_graph,
|
|
16
|
+
filter_graph,
|
|
17
|
+
find_paths,
|
|
18
|
+
graph_summary,
|
|
19
|
+
load_graph,
|
|
20
|
+
write_graph_payload,
|
|
21
|
+
)
|
|
22
|
+
from .live import run_live
|
|
23
|
+
from .semantic import merge_semantic_sidecar
|
|
24
|
+
from .sink import JsonlSink
|
|
25
|
+
from .validate import validate_event_stream
|
|
26
|
+
from .viewer import build_viewer_from_graph
|
|
27
|
+
from .workflow import record_to_viewer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _add_collection_arguments(parser: argparse.ArgumentParser) -> None:
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--watch-root",
|
|
33
|
+
type=Path,
|
|
34
|
+
default=None,
|
|
35
|
+
help="Working directory to observe (default: current directory)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--interval",
|
|
39
|
+
type=float,
|
|
40
|
+
default=0.10,
|
|
41
|
+
help="Portable backend polling interval in seconds (default: 0.10)",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--backend",
|
|
45
|
+
choices=["auto", "portable", "strace"],
|
|
46
|
+
default="auto",
|
|
47
|
+
help="Runtime backend. auto prefers strace on Linux when available",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument("--no-files", action="store_true", help="Disable filesystem observation")
|
|
50
|
+
parser.add_argument("--no-network", action="store_true", help="Disable network observation")
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--keep-native-trace",
|
|
53
|
+
action="store_true",
|
|
54
|
+
help="Keep raw Linux strace files after parsing (off by default)",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _add_live_arguments(parser: argparse.ArgumentParser) -> None:
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--watch-root",
|
|
61
|
+
type=Path,
|
|
62
|
+
default=None,
|
|
63
|
+
help="Working directory to observe (default: current directory)",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--output-dir",
|
|
67
|
+
type=Path,
|
|
68
|
+
default=None,
|
|
69
|
+
help="Artifact directory (default: .execweave/runs/<session-id>/)",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--interval",
|
|
73
|
+
type=float,
|
|
74
|
+
default=0.10,
|
|
75
|
+
help="Portable collector polling interval in seconds (default: 0.10)",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument("--no-files", action="store_true", help="Disable filesystem observation")
|
|
78
|
+
parser.add_argument("--no-network", action="store_true", help="Disable network observation")
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--port",
|
|
81
|
+
type=int,
|
|
82
|
+
default=0,
|
|
83
|
+
help="Localhost port. 0 selects an available port automatically (default: 0)",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--linger",
|
|
87
|
+
type=float,
|
|
88
|
+
default=2.0,
|
|
89
|
+
help="Seconds to keep the live server open after the command exits (default: 2.0)",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--open",
|
|
93
|
+
action="store_true",
|
|
94
|
+
dest="open_browser",
|
|
95
|
+
help="Open the live graph in the default browser",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
100
|
+
parser = argparse.ArgumentParser(
|
|
101
|
+
prog="execweave",
|
|
102
|
+
description="Graph-ready runtime collection for AI agents.",
|
|
103
|
+
)
|
|
104
|
+
subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
|
105
|
+
|
|
106
|
+
run = subparsers.add_parser("run", help="Run a command inside an ExecWeave session")
|
|
107
|
+
_add_collection_arguments(run)
|
|
108
|
+
run.add_argument(
|
|
109
|
+
"--output",
|
|
110
|
+
type=Path,
|
|
111
|
+
default=None,
|
|
112
|
+
help="JSONL output path (default: .execweave/runs/<session-id>.jsonl)",
|
|
113
|
+
)
|
|
114
|
+
run.add_argument("command", nargs=argparse.REMAINDER, help="Command to execute")
|
|
115
|
+
|
|
116
|
+
record = subparsers.add_parser(
|
|
117
|
+
"record",
|
|
118
|
+
help="Record a command, validate it, build the graph, and create the local viewer",
|
|
119
|
+
)
|
|
120
|
+
_add_collection_arguments(record)
|
|
121
|
+
record.add_argument(
|
|
122
|
+
"--output-dir",
|
|
123
|
+
type=Path,
|
|
124
|
+
default=None,
|
|
125
|
+
help="Artifact directory (default: .execweave/runs/<session-id>/)",
|
|
126
|
+
)
|
|
127
|
+
record.add_argument(
|
|
128
|
+
"--open",
|
|
129
|
+
action="store_true",
|
|
130
|
+
dest="open_browser",
|
|
131
|
+
help="Open the generated viewer after the command exits",
|
|
132
|
+
)
|
|
133
|
+
record.add_argument("command", nargs=argparse.REMAINDER, help="Command to execute")
|
|
134
|
+
|
|
135
|
+
live = subparsers.add_parser(
|
|
136
|
+
"live",
|
|
137
|
+
help="Run a command with the portable collector and stream its graph to localhost",
|
|
138
|
+
)
|
|
139
|
+
_add_live_arguments(live)
|
|
140
|
+
live.add_argument("command", nargs=argparse.REMAINDER, help="Command to execute")
|
|
141
|
+
|
|
142
|
+
subparsers.add_parser("doctor", help="Show runtime collector availability")
|
|
143
|
+
|
|
144
|
+
benchmark = subparsers.add_parser(
|
|
145
|
+
"benchmark", help="Run the Phase 1 overhead smoke benchmark"
|
|
146
|
+
)
|
|
147
|
+
benchmark.add_argument(
|
|
148
|
+
"--backend", choices=["auto", "portable", "strace"], default="auto"
|
|
149
|
+
)
|
|
150
|
+
benchmark.add_argument("--iterations", type=int, default=5)
|
|
151
|
+
|
|
152
|
+
validate = subparsers.add_parser(
|
|
153
|
+
"validate", help="Validate one graph-ready ExecWeave JSONL event stream"
|
|
154
|
+
)
|
|
155
|
+
validate.add_argument("path", type=Path, help="Path to a .jsonl event stream")
|
|
156
|
+
validate.add_argument(
|
|
157
|
+
"--allow-incomplete",
|
|
158
|
+
action="store_true",
|
|
159
|
+
help="Do not require session.started/session.finished (useful after an interrupted run)",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
semantic_merge = subparsers.add_parser(
|
|
163
|
+
"semantic-merge",
|
|
164
|
+
help="Merge Agent/Tool/MCP semantic sidecar events into a new runtime event stream",
|
|
165
|
+
)
|
|
166
|
+
semantic_merge.add_argument("runtime", type=Path, help="Validated runtime JSONL stream")
|
|
167
|
+
semantic_merge.add_argument("semantic", type=Path, help="Semantic sidecar JSONL")
|
|
168
|
+
semantic_merge.add_argument("--output", type=Path, required=True)
|
|
169
|
+
|
|
170
|
+
correlate = subparsers.add_parser(
|
|
171
|
+
"correlate",
|
|
172
|
+
help="Add conservative inferred Tool-to-Process correlation edges to a new event stream",
|
|
173
|
+
)
|
|
174
|
+
correlate.add_argument("path", type=Path, help="Validated merged/runtime JSONL stream")
|
|
175
|
+
correlate.add_argument("--output", type=Path, required=True)
|
|
176
|
+
correlate.add_argument(
|
|
177
|
+
"--max-window-ms",
|
|
178
|
+
type=int,
|
|
179
|
+
default=3000,
|
|
180
|
+
help="Maximum Tool-to-Process matching window in milliseconds (default: 3000)",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
graph = subparsers.add_parser(
|
|
184
|
+
"graph", help="Materialize a validated event stream into an execution graph"
|
|
185
|
+
)
|
|
186
|
+
graph.add_argument("path", type=Path, help="Path to a .jsonl event stream")
|
|
187
|
+
graph.add_argument(
|
|
188
|
+
"--output",
|
|
189
|
+
type=Path,
|
|
190
|
+
default=None,
|
|
191
|
+
help="Graph JSON output path (default: <input-stem>.graph.json)",
|
|
192
|
+
)
|
|
193
|
+
graph.add_argument(
|
|
194
|
+
"--allow-incomplete",
|
|
195
|
+
action="store_true",
|
|
196
|
+
help="Allow graph construction from an interrupted but structurally valid run",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
summary = subparsers.add_parser("graph-summary", help="Summarize an execution graph")
|
|
200
|
+
summary.add_argument("path", type=Path, help="Path to a graph JSON file")
|
|
201
|
+
|
|
202
|
+
graph_filter = subparsers.add_parser("graph-filter", help="Filter an execution graph")
|
|
203
|
+
graph_filter.add_argument("path", type=Path, help="Path to a graph JSON file")
|
|
204
|
+
graph_filter.add_argument("--output", type=Path, required=True)
|
|
205
|
+
graph_filter.add_argument("--node-type", action="append", default=[])
|
|
206
|
+
graph_filter.add_argument("--relation", action="append", default=[])
|
|
207
|
+
graph_filter.add_argument("--backend", action="append", default=[])
|
|
208
|
+
graph_filter.add_argument("--causal-only", action="store_true")
|
|
209
|
+
|
|
210
|
+
graph_focus = subparsers.add_parser(
|
|
211
|
+
"graph-focus",
|
|
212
|
+
help="Extract an evidence-preserving N-hop neighborhood around one or more nodes",
|
|
213
|
+
)
|
|
214
|
+
graph_focus.add_argument("path", type=Path, help="Path to a graph JSON file")
|
|
215
|
+
graph_focus.add_argument("anchor", nargs="+", help="Exact anchor node ID(s)")
|
|
216
|
+
graph_focus.add_argument("--output", type=Path, required=True)
|
|
217
|
+
graph_focus.add_argument(
|
|
218
|
+
"--hops",
|
|
219
|
+
type=int,
|
|
220
|
+
default=1,
|
|
221
|
+
help="Maximum traversal distance from each anchor (default: 1)",
|
|
222
|
+
)
|
|
223
|
+
graph_focus.add_argument(
|
|
224
|
+
"--direction",
|
|
225
|
+
choices=["both", "in", "out"],
|
|
226
|
+
default="both",
|
|
227
|
+
help="Traverse incoming, outgoing, or both edge directions (default: both)",
|
|
228
|
+
)
|
|
229
|
+
graph_focus.add_argument("--relation", action="append", default=[])
|
|
230
|
+
graph_focus.add_argument("--causal-only", action="store_true")
|
|
231
|
+
|
|
232
|
+
graph_condense = subparsers.add_parser(
|
|
233
|
+
"graph-condense",
|
|
234
|
+
help="Collapse repetitive leaf resources into inspectable cluster nodes",
|
|
235
|
+
)
|
|
236
|
+
graph_condense.add_argument("path", type=Path, help="Path to a graph JSON file")
|
|
237
|
+
graph_condense.add_argument("--output", type=Path, required=True)
|
|
238
|
+
graph_condense.add_argument(
|
|
239
|
+
"--threshold",
|
|
240
|
+
type=int,
|
|
241
|
+
default=8,
|
|
242
|
+
help="Minimum equivalent leaf nodes required to form a cluster (default: 8)",
|
|
243
|
+
)
|
|
244
|
+
graph_condense.add_argument(
|
|
245
|
+
"--sample-size",
|
|
246
|
+
type=int,
|
|
247
|
+
default=8,
|
|
248
|
+
help="Maximum member names stored as cluster examples (default: 8)",
|
|
249
|
+
)
|
|
250
|
+
graph_condense.add_argument(
|
|
251
|
+
"--keep-expansion",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="Embed original cluster members so the Viewer can expand them on demand",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
analyze = subparsers.add_parser(
|
|
257
|
+
"analyze",
|
|
258
|
+
help="Run conservative explainable security rules over an execution graph",
|
|
259
|
+
)
|
|
260
|
+
analyze.add_argument("graph", type=Path, help="Path to a graph JSON file")
|
|
261
|
+
analyze.add_argument(
|
|
262
|
+
"--output",
|
|
263
|
+
type=Path,
|
|
264
|
+
default=None,
|
|
265
|
+
help="Optional JSON report output path; findings are always printed to stdout",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
path_query = subparsers.add_parser("path", help="Find directed paths in an execution graph")
|
|
269
|
+
path_query.add_argument("graph", type=Path, help="Path to a graph JSON file")
|
|
270
|
+
path_query.add_argument("source", help="Exact source node ID")
|
|
271
|
+
path_query.add_argument("target", help="Exact target node ID")
|
|
272
|
+
path_query.add_argument("--max-depth", type=int, default=6)
|
|
273
|
+
path_query.add_argument("--max-paths", type=int, default=20)
|
|
274
|
+
path_query.add_argument("--relation", action="append", default=[])
|
|
275
|
+
path_query.add_argument("--causal-only", action="store_true")
|
|
276
|
+
|
|
277
|
+
view = subparsers.add_parser(
|
|
278
|
+
"view", help="Create a standalone local interactive HTML graph viewer"
|
|
279
|
+
)
|
|
280
|
+
view.add_argument("graph", type=Path, help="Path to a graph JSON file")
|
|
281
|
+
view.add_argument(
|
|
282
|
+
"--output",
|
|
283
|
+
type=Path,
|
|
284
|
+
default=None,
|
|
285
|
+
help="HTML output path (default: <graph-stem>.html)",
|
|
286
|
+
)
|
|
287
|
+
view.add_argument(
|
|
288
|
+
"--open",
|
|
289
|
+
action="store_true",
|
|
290
|
+
dest="open_browser",
|
|
291
|
+
help="Open the generated viewer in the default browser",
|
|
292
|
+
)
|
|
293
|
+
return parser
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _clean_command(command: list[str]) -> list[str]:
|
|
297
|
+
result = list(command)
|
|
298
|
+
if result and result[0] == "--":
|
|
299
|
+
result = result[1:]
|
|
300
|
+
return result
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def main(argv: list[str] | None = None) -> int:
|
|
304
|
+
parser = build_parser()
|
|
305
|
+
args = parser.parse_args(argv)
|
|
306
|
+
|
|
307
|
+
if args.subcommand == "doctor":
|
|
308
|
+
print(json.dumps(backend_diagnostics(), indent=2, sort_keys=True))
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
if args.subcommand == "benchmark":
|
|
312
|
+
try:
|
|
313
|
+
result = run_benchmark(backend=args.backend, iterations=args.iterations)
|
|
314
|
+
except (RuntimeError, ValueError) as exc:
|
|
315
|
+
parser.error(str(exc))
|
|
316
|
+
print(format_benchmark(result))
|
|
317
|
+
return 0
|
|
318
|
+
|
|
319
|
+
if args.subcommand == "validate":
|
|
320
|
+
result = validate_event_stream(
|
|
321
|
+
args.path,
|
|
322
|
+
require_complete_session=not args.allow_incomplete,
|
|
323
|
+
)
|
|
324
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
325
|
+
return 0 if result.valid else 1
|
|
326
|
+
|
|
327
|
+
if args.subcommand == "semantic-merge":
|
|
328
|
+
try:
|
|
329
|
+
result = merge_semantic_sidecar(args.runtime, args.semantic, args.output)
|
|
330
|
+
except (FileExistsError, ValueError) as exc:
|
|
331
|
+
parser.error(str(exc))
|
|
332
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
333
|
+
return 0
|
|
334
|
+
|
|
335
|
+
if args.subcommand == "correlate":
|
|
336
|
+
try:
|
|
337
|
+
result = correlate_tool_process(
|
|
338
|
+
args.path,
|
|
339
|
+
args.output,
|
|
340
|
+
max_window_ms=args.max_window_ms,
|
|
341
|
+
)
|
|
342
|
+
except (FileExistsError, ValueError) as exc:
|
|
343
|
+
parser.error(str(exc))
|
|
344
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
345
|
+
return 0
|
|
346
|
+
|
|
347
|
+
if args.subcommand == "graph":
|
|
348
|
+
source = args.path.expanduser().resolve()
|
|
349
|
+
output = args.output or source.with_name(f"{source.stem}.graph.json")
|
|
350
|
+
try:
|
|
351
|
+
execution_graph = build_execution_graph(
|
|
352
|
+
source,
|
|
353
|
+
allow_incomplete=args.allow_incomplete,
|
|
354
|
+
)
|
|
355
|
+
written = write_execution_graph(execution_graph, output)
|
|
356
|
+
except (FileExistsError, ValueError) as exc:
|
|
357
|
+
parser.error(str(exc))
|
|
358
|
+
summary = {
|
|
359
|
+
"session_id": execution_graph.session_id,
|
|
360
|
+
"event_count": execution_graph.event_count,
|
|
361
|
+
"node_count": len(execution_graph.nodes),
|
|
362
|
+
"edge_count": len(execution_graph.edges),
|
|
363
|
+
"output": str(written),
|
|
364
|
+
}
|
|
365
|
+
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
366
|
+
return 0
|
|
367
|
+
|
|
368
|
+
if args.subcommand == "graph-summary":
|
|
369
|
+
try:
|
|
370
|
+
payload = load_graph(args.path)
|
|
371
|
+
except ValueError as exc:
|
|
372
|
+
parser.error(str(exc))
|
|
373
|
+
print(json.dumps(graph_summary(payload), indent=2, sort_keys=True))
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
if args.subcommand == "graph-filter":
|
|
377
|
+
try:
|
|
378
|
+
payload = load_graph(args.path)
|
|
379
|
+
filtered = filter_graph(
|
|
380
|
+
payload,
|
|
381
|
+
node_types=args.node_type,
|
|
382
|
+
relations=args.relation,
|
|
383
|
+
causal_only=args.causal_only,
|
|
384
|
+
backends=args.backend,
|
|
385
|
+
)
|
|
386
|
+
written = write_graph_payload(filtered, args.output)
|
|
387
|
+
except (FileExistsError, ValueError) as exc:
|
|
388
|
+
parser.error(str(exc))
|
|
389
|
+
print(
|
|
390
|
+
json.dumps(
|
|
391
|
+
{**graph_summary(filtered), "output": str(written)},
|
|
392
|
+
indent=2,
|
|
393
|
+
sort_keys=True,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
return 0
|
|
397
|
+
|
|
398
|
+
if args.subcommand == "graph-focus":
|
|
399
|
+
try:
|
|
400
|
+
payload = load_graph(args.path)
|
|
401
|
+
focused = focus_graph(
|
|
402
|
+
payload,
|
|
403
|
+
anchors=args.anchor,
|
|
404
|
+
hops=args.hops,
|
|
405
|
+
direction=args.direction,
|
|
406
|
+
relations=args.relation,
|
|
407
|
+
causal_only=args.causal_only,
|
|
408
|
+
)
|
|
409
|
+
written = write_graph_payload(focused, args.output)
|
|
410
|
+
except (FileExistsError, ValueError) as exc:
|
|
411
|
+
parser.error(str(exc))
|
|
412
|
+
print(
|
|
413
|
+
json.dumps(
|
|
414
|
+
{
|
|
415
|
+
**graph_summary(focused),
|
|
416
|
+
"focus": focused.get("focus"),
|
|
417
|
+
"output": str(written),
|
|
418
|
+
},
|
|
419
|
+
indent=2,
|
|
420
|
+
sort_keys=True,
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
return 0
|
|
424
|
+
|
|
425
|
+
if args.subcommand == "graph-condense":
|
|
426
|
+
try:
|
|
427
|
+
payload = load_graph(args.path)
|
|
428
|
+
condensed = condense_graph(
|
|
429
|
+
payload,
|
|
430
|
+
threshold=args.threshold,
|
|
431
|
+
sample_size=args.sample_size,
|
|
432
|
+
include_expansion=args.keep_expansion,
|
|
433
|
+
)
|
|
434
|
+
written = write_graph_payload(condensed, args.output)
|
|
435
|
+
except (FileExistsError, ValueError) as exc:
|
|
436
|
+
parser.error(str(exc))
|
|
437
|
+
print(
|
|
438
|
+
json.dumps(
|
|
439
|
+
{
|
|
440
|
+
**graph_summary(condensed),
|
|
441
|
+
"condensation": condensed.get("condensation"),
|
|
442
|
+
"output": str(written),
|
|
443
|
+
},
|
|
444
|
+
indent=2,
|
|
445
|
+
sort_keys=True,
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
return 0
|
|
449
|
+
|
|
450
|
+
if args.subcommand == "analyze":
|
|
451
|
+
try:
|
|
452
|
+
payload = load_graph(args.graph)
|
|
453
|
+
report = analyze_graph(payload)
|
|
454
|
+
if args.output is not None:
|
|
455
|
+
output = args.output.expanduser().resolve()
|
|
456
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
457
|
+
if output.exists() and output.stat().st_size > 0:
|
|
458
|
+
raise FileExistsError(f"ExecWeave analysis output already exists: {output}")
|
|
459
|
+
output.write_text(
|
|
460
|
+
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
|
461
|
+
encoding="utf-8",
|
|
462
|
+
)
|
|
463
|
+
except (FileExistsError, ValueError) as exc:
|
|
464
|
+
parser.error(str(exc))
|
|
465
|
+
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
|
|
466
|
+
return 0
|
|
467
|
+
|
|
468
|
+
if args.subcommand == "path":
|
|
469
|
+
try:
|
|
470
|
+
payload = load_graph(args.graph)
|
|
471
|
+
paths = find_paths(
|
|
472
|
+
payload,
|
|
473
|
+
source=args.source,
|
|
474
|
+
target=args.target,
|
|
475
|
+
max_depth=args.max_depth,
|
|
476
|
+
max_paths=args.max_paths,
|
|
477
|
+
relations=args.relation,
|
|
478
|
+
causal_only=args.causal_only,
|
|
479
|
+
)
|
|
480
|
+
except ValueError as exc:
|
|
481
|
+
parser.error(str(exc))
|
|
482
|
+
print(
|
|
483
|
+
json.dumps(
|
|
484
|
+
{
|
|
485
|
+
"source": args.source,
|
|
486
|
+
"target": args.target,
|
|
487
|
+
"path_count": len(paths),
|
|
488
|
+
"paths": paths,
|
|
489
|
+
},
|
|
490
|
+
indent=2,
|
|
491
|
+
sort_keys=True,
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
return 0
|
|
495
|
+
|
|
496
|
+
if args.subcommand == "view":
|
|
497
|
+
graph_path = args.graph.expanduser().resolve()
|
|
498
|
+
output = args.output or graph_path.with_name(f"{graph_path.stem}.html")
|
|
499
|
+
try:
|
|
500
|
+
written = build_viewer_from_graph(
|
|
501
|
+
graph_path,
|
|
502
|
+
output,
|
|
503
|
+
open_browser=args.open_browser,
|
|
504
|
+
)
|
|
505
|
+
except (FileExistsError, ValueError) as exc:
|
|
506
|
+
parser.error(str(exc))
|
|
507
|
+
print(json.dumps({"output": str(written)}, indent=2, sort_keys=True))
|
|
508
|
+
return 0
|
|
509
|
+
|
|
510
|
+
if args.subcommand == "record":
|
|
511
|
+
command = _clean_command(args.command)
|
|
512
|
+
if not command:
|
|
513
|
+
parser.error(
|
|
514
|
+
"execweave record requires a command, e.g. execweave record --open -- claude"
|
|
515
|
+
)
|
|
516
|
+
watch_root = (args.watch_root or Path.cwd()).expanduser().resolve()
|
|
517
|
+
try:
|
|
518
|
+
result = record_to_viewer(
|
|
519
|
+
command,
|
|
520
|
+
watch_root=watch_root,
|
|
521
|
+
output_dir=args.output_dir,
|
|
522
|
+
backend=args.backend,
|
|
523
|
+
poll_interval=args.interval,
|
|
524
|
+
collect_filesystem=not args.no_files,
|
|
525
|
+
collect_network=not args.no_network,
|
|
526
|
+
keep_raw_trace=args.keep_native_trace,
|
|
527
|
+
open_browser=args.open_browser,
|
|
528
|
+
)
|
|
529
|
+
except (FileExistsError, RuntimeError, ValueError) as exc:
|
|
530
|
+
parser.error(str(exc))
|
|
531
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
532
|
+
return result.return_code
|
|
533
|
+
|
|
534
|
+
if args.subcommand == "live":
|
|
535
|
+
command = _clean_command(args.command)
|
|
536
|
+
if not command:
|
|
537
|
+
parser.error("execweave live requires a command, e.g. execweave live --open -- claude")
|
|
538
|
+
watch_root = (args.watch_root or Path.cwd()).expanduser().resolve()
|
|
539
|
+
try:
|
|
540
|
+
result = run_live(
|
|
541
|
+
command,
|
|
542
|
+
watch_root=watch_root,
|
|
543
|
+
output_dir=args.output_dir,
|
|
544
|
+
poll_interval=args.interval,
|
|
545
|
+
collect_filesystem=not args.no_files,
|
|
546
|
+
collect_network=not args.no_network,
|
|
547
|
+
port=args.port,
|
|
548
|
+
open_browser=args.open_browser,
|
|
549
|
+
linger_seconds=args.linger,
|
|
550
|
+
announce=lambda url: print(f"ExecWeave live: {url}", flush=True),
|
|
551
|
+
)
|
|
552
|
+
except (FileExistsError, RuntimeError, ValueError, OSError) as exc:
|
|
553
|
+
parser.error(str(exc))
|
|
554
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
555
|
+
return result.return_code
|
|
556
|
+
|
|
557
|
+
command = _clean_command(args.command)
|
|
558
|
+
if not command:
|
|
559
|
+
parser.error("execweave run requires a command, e.g. execweave run -- claude")
|
|
560
|
+
|
|
561
|
+
session_id = uuid4().hex
|
|
562
|
+
watch_root = (args.watch_root or Path.cwd()).expanduser().resolve()
|
|
563
|
+
output = args.output or (watch_root / ".execweave" / "runs" / f"{session_id}.jsonl")
|
|
564
|
+
try:
|
|
565
|
+
sink = JsonlSink(output)
|
|
566
|
+
except FileExistsError as exc:
|
|
567
|
+
parser.error(str(exc))
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
resolved = resolve_backend(args.backend)
|
|
571
|
+
collector = create_collector(
|
|
572
|
+
backend=args.backend,
|
|
573
|
+
session_id=session_id,
|
|
574
|
+
sink=sink,
|
|
575
|
+
watch_root=watch_root,
|
|
576
|
+
poll_interval=args.interval,
|
|
577
|
+
collect_filesystem=not args.no_files,
|
|
578
|
+
collect_network=not args.no_network,
|
|
579
|
+
keep_raw_trace=args.keep_native_trace,
|
|
580
|
+
)
|
|
581
|
+
except RuntimeError as exc:
|
|
582
|
+
parser.error(str(exc))
|
|
583
|
+
|
|
584
|
+
print(f"ExecWeave session: {session_id}")
|
|
585
|
+
print(f"Backend: {resolved}")
|
|
586
|
+
print(f"Working directory: {watch_root}")
|
|
587
|
+
print(f"Events: {sink.path}")
|
|
588
|
+
return collector.run(command)
|