copilot-cli-trace-deck 0.1.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.
- copilot_cli_trace_deck/__init__.py +1 -0
- copilot_cli_trace_deck/__main__.py +5 -0
- copilot_cli_trace_deck/data/__init__.py +5 -0
- copilot_cli_trace_deck/data/sessions.py +628 -0
- copilot_cli_trace_deck/models.py +61 -0
- copilot_cli_trace_deck/server.py +3 -0
- copilot_cli_trace_deck/web/__init__.py +1 -0
- copilot_cli_trace_deck/web/pages.py +2305 -0
- copilot_cli_trace_deck/web/server.py +315 -0
- copilot_cli_trace_deck-0.1.0.dist-info/METADATA +63 -0
- copilot_cli_trace_deck-0.1.0.dist-info/RECORD +15 -0
- copilot_cli_trace_deck-0.1.0.dist-info/WHEEL +5 -0
- copilot_cli_trace_deck-0.1.0.dist-info/entry_points.txt +2 -0
- copilot_cli_trace_deck-0.1.0.dist-info/licenses/LICENSE +21 -0
- copilot_cli_trace_deck-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import webbrowser
|
|
8
|
+
from http import HTTPStatus
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import parse_qs, urlsplit
|
|
12
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
13
|
+
from watchdog.observers import Observer
|
|
14
|
+
|
|
15
|
+
from ..data.sessions import load_session_flow, load_session_logs, load_session_previews, load_session_summary
|
|
16
|
+
from .pages import PAGE_TITLE, build_flow_feed_payload, build_flow_page, build_index_feed_payload, build_index_page, build_logs_feed_payload, build_logs_page, build_missing_session_page, build_session_page, build_session_snapshot_payload
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_SESSION_ROOT = Path.home() / ".copilot" / "session-state"
|
|
20
|
+
RELEVANT_SESSION_FILES = {"events.jsonl", "workspace.yaml"}
|
|
21
|
+
SSE_HEARTBEAT_INTERVAL = 15.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_server_url(host: str, port: int) -> str:
|
|
25
|
+
return f"http://{host}:{port}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def serialize_payload(payload: dict[str, object]) -> str:
|
|
29
|
+
return json.dumps(payload, ensure_ascii=False, sort_keys=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_relevant_session_path(raw_path: str, session_root: Path) -> bool:
|
|
33
|
+
if not raw_path:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
path = Path(raw_path).expanduser().resolve(strict=False)
|
|
37
|
+
normalized_root = session_root.expanduser().resolve(strict=False)
|
|
38
|
+
if path.name not in RELEVANT_SESSION_FILES:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
path.relative_to(normalized_root)
|
|
43
|
+
except ValueError:
|
|
44
|
+
return False
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SessionStateChangeHandler(FileSystemEventHandler):
|
|
49
|
+
def __init__(self, broadcaster: "SessionStateChangeBroadcaster") -> None:
|
|
50
|
+
self.broadcaster = broadcaster
|
|
51
|
+
|
|
52
|
+
def on_any_event(self, event: FileSystemEvent) -> None:
|
|
53
|
+
if event.is_directory:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
src_path = getattr(event, "src_path", "")
|
|
57
|
+
dest_path = getattr(event, "dest_path", "")
|
|
58
|
+
if is_relevant_session_path(src_path, self.broadcaster.session_root) or is_relevant_session_path(dest_path, self.broadcaster.session_root):
|
|
59
|
+
self.broadcaster.notify_change()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SessionStateChangeBroadcaster:
|
|
63
|
+
def __init__(self, session_root: Path) -> None:
|
|
64
|
+
self.session_root = session_root.expanduser()
|
|
65
|
+
self._condition = threading.Condition()
|
|
66
|
+
self._version = 0
|
|
67
|
+
self._observer: Observer | None = None
|
|
68
|
+
|
|
69
|
+
def start(self) -> None:
|
|
70
|
+
watch_root = self.session_root if self.session_root.exists() else self.session_root.parent
|
|
71
|
+
observer = Observer()
|
|
72
|
+
observer.schedule(SessionStateChangeHandler(self), str(watch_root), recursive=True)
|
|
73
|
+
observer.start()
|
|
74
|
+
self._observer = observer
|
|
75
|
+
|
|
76
|
+
def stop(self) -> None:
|
|
77
|
+
observer = self._observer
|
|
78
|
+
if observer is None:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
observer.stop()
|
|
82
|
+
observer.join(timeout=5)
|
|
83
|
+
self._observer = None
|
|
84
|
+
|
|
85
|
+
def current_version(self) -> int:
|
|
86
|
+
with self._condition:
|
|
87
|
+
return self._version
|
|
88
|
+
|
|
89
|
+
def notify_change(self) -> None:
|
|
90
|
+
with self._condition:
|
|
91
|
+
self._version += 1
|
|
92
|
+
self._condition.notify_all()
|
|
93
|
+
|
|
94
|
+
def wait_for_change(self, version: int, timeout: float) -> int:
|
|
95
|
+
with self._condition:
|
|
96
|
+
if self._version != version:
|
|
97
|
+
return self._version
|
|
98
|
+
|
|
99
|
+
self._condition.wait(timeout=timeout)
|
|
100
|
+
return self._version
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TraceDeckHandler(BaseHTTPRequestHandler):
|
|
104
|
+
session_root = DEFAULT_SESSION_ROOT
|
|
105
|
+
change_broadcaster: SessionStateChangeBroadcaster | None = None
|
|
106
|
+
|
|
107
|
+
def handle(self) -> None:
|
|
108
|
+
try:
|
|
109
|
+
super().handle()
|
|
110
|
+
except (BrokenPipeError, ConnectionResetError, TimeoutError):
|
|
111
|
+
self.close_connection = True
|
|
112
|
+
|
|
113
|
+
def do_GET(self) -> None: # noqa: N802
|
|
114
|
+
parsed_url = urlsplit(self.path)
|
|
115
|
+
path = parsed_url.path
|
|
116
|
+
query = parse_qs(parsed_url.query)
|
|
117
|
+
if path == "/index.events":
|
|
118
|
+
self.respond_event_stream(lambda: serialize_payload(build_index_feed_payload(load_session_previews(self.session_root))))
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
if path == "/index.json":
|
|
122
|
+
self.respond_json(build_index_feed_payload(load_session_previews(self.session_root)), HTTPStatus.OK)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if path in {"/", "/index.html"}:
|
|
126
|
+
self.respond_html(build_index_page(load_session_previews(self.session_root)), HTTPStatus.OK)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
path_parts = [part for part in path.split("/") if part]
|
|
130
|
+
if path_parts[:1] == ["sessions"]:
|
|
131
|
+
if len(path_parts) < 2:
|
|
132
|
+
self.send_error(HTTPStatus.NOT_FOUND, "Not Found")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
session_id = path_parts[1]
|
|
136
|
+
summary = load_session_summary(self.session_root, session_id)
|
|
137
|
+
if summary is None:
|
|
138
|
+
self.respond_html(build_missing_session_page(session_id), HTTPStatus.NOT_FOUND)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if len(path_parts) == 3 and path_parts[2] == "logs":
|
|
142
|
+
log_entries = load_session_logs(self.session_root, session_id)
|
|
143
|
+
self.respond_html(build_logs_page(summary, log_entries or []), HTTPStatus.OK)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if len(path_parts) == 3 and path_parts[2] == "summary.events":
|
|
147
|
+
self.respond_event_stream(
|
|
148
|
+
lambda: serialize_payload(build_session_snapshot_payload(load_session_summary(self.session_root, session_id) or summary))
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if len(path_parts) == 3 and path_parts[2] == "summary.json":
|
|
153
|
+
self.respond_json(build_session_snapshot_payload(summary), HTTPStatus.OK)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if len(path_parts) == 3 and path_parts[2] == "logs.events":
|
|
157
|
+
self.respond_event_stream(
|
|
158
|
+
lambda: serialize_payload(build_logs_feed_payload(load_session_logs(self.session_root, session_id) or [], -1))
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if len(path_parts) == 3 and path_parts[2] == "logs.json":
|
|
163
|
+
after_value = query.get("after", ["-1"])[0]
|
|
164
|
+
try:
|
|
165
|
+
after_index = int(after_value)
|
|
166
|
+
except ValueError:
|
|
167
|
+
after_index = -1
|
|
168
|
+
|
|
169
|
+
log_entries = load_session_logs(self.session_root, session_id) or []
|
|
170
|
+
payload = build_logs_feed_payload(log_entries, after_index)
|
|
171
|
+
self.respond_json(payload, HTTPStatus.OK)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if len(path_parts) == 3 and path_parts[2] == "flow":
|
|
175
|
+
flow_nodes = load_session_flow(self.session_root, session_id)
|
|
176
|
+
self.respond_html(build_flow_page(summary, flow_nodes or []), HTTPStatus.OK)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if len(path_parts) == 3 and path_parts[2] == "flow.events":
|
|
180
|
+
self.respond_event_stream(
|
|
181
|
+
lambda: serialize_payload(
|
|
182
|
+
build_flow_feed_payload(
|
|
183
|
+
session_id,
|
|
184
|
+
load_session_summary(self.session_root, session_id) or summary,
|
|
185
|
+
load_session_flow(self.session_root, session_id) or [],
|
|
186
|
+
-1,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if len(path_parts) == 3 and path_parts[2] == "flow.json":
|
|
193
|
+
after_value = query.get("after", ["-1"])[0]
|
|
194
|
+
try:
|
|
195
|
+
after_index = int(after_value)
|
|
196
|
+
except ValueError:
|
|
197
|
+
after_index = -1
|
|
198
|
+
|
|
199
|
+
flow_nodes = load_session_flow(self.session_root, session_id) or []
|
|
200
|
+
payload = build_flow_feed_payload(session_id, summary, flow_nodes, after_index)
|
|
201
|
+
self.respond_json(payload, HTTPStatus.OK)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if len(path_parts) == 2:
|
|
205
|
+
self.respond_html(build_session_page(summary), HTTPStatus.OK)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
self.send_error(HTTPStatus.NOT_FOUND, "Not Found")
|
|
209
|
+
|
|
210
|
+
def respond_html(self, page: str, status: HTTPStatus) -> None:
|
|
211
|
+
body = page.encode("utf-8")
|
|
212
|
+
self.send_response(status)
|
|
213
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
214
|
+
self.send_header("Content-Length", str(len(body)))
|
|
215
|
+
self.end_headers()
|
|
216
|
+
self.wfile.write(body)
|
|
217
|
+
|
|
218
|
+
def respond_json(self, payload: dict[str, object], status: HTTPStatus) -> None:
|
|
219
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
220
|
+
self.send_response(status)
|
|
221
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
222
|
+
self.send_header("Content-Length", str(len(body)))
|
|
223
|
+
self.end_headers()
|
|
224
|
+
self.wfile.write(body)
|
|
225
|
+
|
|
226
|
+
def respond_event_stream(self, snapshot_loader) -> None:
|
|
227
|
+
self.close_connection = True
|
|
228
|
+
self.send_response(HTTPStatus.OK)
|
|
229
|
+
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
230
|
+
self.send_header("Cache-Control", "no-cache")
|
|
231
|
+
self.send_header("Connection", "keep-alive")
|
|
232
|
+
self.send_header("X-Accel-Buffering", "no")
|
|
233
|
+
self.end_headers()
|
|
234
|
+
|
|
235
|
+
broadcaster = self.change_broadcaster
|
|
236
|
+
version = broadcaster.current_version() if broadcaster else 0
|
|
237
|
+
previous_snapshot = snapshot_loader()
|
|
238
|
+
last_heartbeat = time.monotonic()
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
self.write_event_stream_message("update")
|
|
242
|
+
while True:
|
|
243
|
+
if broadcaster is None:
|
|
244
|
+
time.sleep(SSE_HEARTBEAT_INTERVAL)
|
|
245
|
+
self.write_event_stream_comment("keepalive")
|
|
246
|
+
last_heartbeat = time.monotonic()
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
remaining = max(0.0, SSE_HEARTBEAT_INTERVAL - (time.monotonic() - last_heartbeat))
|
|
250
|
+
next_version = broadcaster.wait_for_change(version, remaining)
|
|
251
|
+
now = time.monotonic()
|
|
252
|
+
if next_version != version:
|
|
253
|
+
version = next_version
|
|
254
|
+
snapshot = snapshot_loader()
|
|
255
|
+
if snapshot != previous_snapshot:
|
|
256
|
+
self.write_event_stream_message("update")
|
|
257
|
+
previous_snapshot = snapshot
|
|
258
|
+
last_heartbeat = now
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
if now - last_heartbeat >= SSE_HEARTBEAT_INTERVAL:
|
|
262
|
+
self.write_event_stream_comment("keepalive")
|
|
263
|
+
last_heartbeat = now
|
|
264
|
+
except (BrokenPipeError, ConnectionResetError, TimeoutError, ValueError):
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
def write_event_stream_comment(self, comment: str) -> None:
|
|
268
|
+
self.wfile.write(f": {comment}\n\n".encode("utf-8"))
|
|
269
|
+
self.wfile.flush()
|
|
270
|
+
|
|
271
|
+
def write_event_stream_message(self, data: str) -> None:
|
|
272
|
+
self.wfile.write(f"data: {data}\n\n".encode("utf-8"))
|
|
273
|
+
self.wfile.flush()
|
|
274
|
+
|
|
275
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def parse_args() -> argparse.Namespace:
|
|
280
|
+
parser = argparse.ArgumentParser(description=PAGE_TITLE)
|
|
281
|
+
parser.add_argument("source", nargs="?", default=str(DEFAULT_SESSION_ROOT), help="Session-state root to read")
|
|
282
|
+
parser.add_argument("--host", default="127.0.0.1", help="Host interface to bind")
|
|
283
|
+
parser.add_argument("--port", type=int, default=9887, help="Port to listen on")
|
|
284
|
+
parser.add_argument(
|
|
285
|
+
"--quiet",
|
|
286
|
+
"--quite",
|
|
287
|
+
action="store_true",
|
|
288
|
+
dest="quiet",
|
|
289
|
+
help="Suppress the startup banner and do not open a browser automatically",
|
|
290
|
+
)
|
|
291
|
+
return parser.parse_args()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def main() -> int:
|
|
295
|
+
args = parse_args()
|
|
296
|
+
TraceDeckHandler.session_root = Path(args.source).expanduser()
|
|
297
|
+
TraceDeckHandler.change_broadcaster = SessionStateChangeBroadcaster(TraceDeckHandler.session_root)
|
|
298
|
+
TraceDeckHandler.change_broadcaster.start()
|
|
299
|
+
server = ThreadingHTTPServer((args.host, args.port), TraceDeckHandler)
|
|
300
|
+
url = build_server_url(args.host, args.port)
|
|
301
|
+
if not args.quiet:
|
|
302
|
+
print(f"Serving {PAGE_TITLE} on {url}")
|
|
303
|
+
webbrowser.open(url)
|
|
304
|
+
else:
|
|
305
|
+
print(url)
|
|
306
|
+
try:
|
|
307
|
+
server.serve_forever()
|
|
308
|
+
except KeyboardInterrupt:
|
|
309
|
+
pass
|
|
310
|
+
finally:
|
|
311
|
+
server.server_close()
|
|
312
|
+
if TraceDeckHandler.change_broadcaster is not None:
|
|
313
|
+
TraceDeckHandler.change_broadcaster.stop()
|
|
314
|
+
TraceDeckHandler.change_broadcaster = None
|
|
315
|
+
return 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: copilot-cli-trace-deck
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: watchdog<7,>=6
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# copilot-cli-trace-deck
|
|
12
|
+
Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view.
|
|
13
|
+
|
|
14
|
+
## Screenshots
|
|
15
|
+
|
|
16
|
+
### Main View
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
### Summary View
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
### Log View
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### Flow View
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
## Run
|
|
33
|
+
|
|
34
|
+
Run the app directly from the workspace with `uv`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv run copilot-cli-trace-deck
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
By default the server listens on `http://127.0.0.1:9887` and opens that URL in your browser.
|
|
41
|
+
|
|
42
|
+
Live updates are pushed over Server-Sent Events and triggered by file-system changes in the session-state directory, so the home, summary, logs, and flow pages update without browser polling.
|
|
43
|
+
|
|
44
|
+
You can also pass the session-state source and server options:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv run copilot-cli-trace-deck ~/.copilot/session-state --host 127.0.0.1 --port 9887
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To skip opening a browser while still printing the local URL:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run copilot-cli-trace-deck --quiet
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Install As A Command
|
|
57
|
+
|
|
58
|
+
Install the project as a local tool and run it directly from your shell:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv tool install .
|
|
62
|
+
copilot-cli-trace-deck --help
|
|
63
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
copilot_cli_trace_deck/__init__.py,sha256=vbXuXZEIh4WLwR9bVHeD5aB6yXt7H7zDGwx_GWMSfGo,38
|
|
2
|
+
copilot_cli_trace_deck/__main__.py,sha256=UlvULmcpjPvRsgFaSDEc0343rMArSBe7ZeRrCcoIJyo,87
|
|
3
|
+
copilot_cli_trace_deck/models.py,sha256=ug7bi2mWN4EuARGITslcoS5jYKNWfowXru1MRCcndoc,1097
|
|
4
|
+
copilot_cli_trace_deck/server.py,sha256=E7lBuWEg_Ipbf2bMIAuTKpc_QqEW0rL8mptczToUVdo,159
|
|
5
|
+
copilot_cli_trace_deck/data/__init__.py,sha256=x4RE-d6qnTA7qPTDNb8dTdTBPBzBm38RTfyIWvRpqMk,164
|
|
6
|
+
copilot_cli_trace_deck/data/sessions.py,sha256=YtZ2oYm_8Ai2uuDD3OniVofGx3U5SoO54J-f_cCl6X0,23792
|
|
7
|
+
copilot_cli_trace_deck/web/__init__.py,sha256=gNE_oMKNIJOMXgtkP5Q_dzqJEW82LesNrYlqo434eNo,45
|
|
8
|
+
copilot_cli_trace_deck/web/pages.py,sha256=KAgDoXUD0sJQAqtcJg1InSrP1EENzTqlq4BFO8mu7hU,69531
|
|
9
|
+
copilot_cli_trace_deck/web/server.py,sha256=yZNCupg0Zsk2FhatOyVc9ZlfN8h5F8eZFB2gysxJlhk,12385
|
|
10
|
+
copilot_cli_trace_deck-0.1.0.dist-info/licenses/LICENSE,sha256=hYRaJ2ed2IMo9RmO96UxUSTt9iwL49leBY5kVtDymRo,1063
|
|
11
|
+
copilot_cli_trace_deck-0.1.0.dist-info/METADATA,sha256=Bk-t9y8WFHLgrhbVOLvF9KR0eqoeaNyyDB2e117Dg98,1412
|
|
12
|
+
copilot_cli_trace_deck-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
copilot_cli_trace_deck-0.1.0.dist-info/entry_points.txt,sha256=3uF4fO40GrZ5EDqNfJAR4fa3HuGnPk_ysx7vrCU8Nos,80
|
|
14
|
+
copilot_cli_trace_deck-0.1.0.dist-info/top_level.txt,sha256=_uXXkSLhf1rQn-WOeTo7ms2PCWcjRKGj5KCOQCJTJ6g,23
|
|
15
|
+
copilot_cli_trace_deck-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lanbao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
copilot_cli_trace_deck
|