results-cli 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.
- results/__init__.py +3 -0
- results/cli.py +277 -0
- results/ledger.py +78 -0
- results_cli-0.1.0.dist-info/METADATA +114 -0
- results_cli-0.1.0.dist-info/RECORD +8 -0
- results_cli-0.1.0.dist-info/WHEEL +4 -0
- results_cli-0.1.0.dist-info/entry_points.txt +2 -0
- results_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
results/__init__.py
ADDED
results/cli.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Seal a run, record what it produced, and verify the chain.
|
|
2
|
+
|
|
3
|
+
results init start tracking results here
|
|
4
|
+
results seal <file>... hash inputs before a run (prereg, script, data)
|
|
5
|
+
results access <note> record a data-access event (what you looked at, when)
|
|
6
|
+
results run <file>... record outputs after a run completes
|
|
7
|
+
results claim <text> bind a manuscript claim to a run's output
|
|
8
|
+
results verify check the ledger chain and every hash it names
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import pathlib
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from results import ledger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
RESULTS_DIR = ".results"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_root(start: pathlib.Path | None = None) -> pathlib.Path | None:
|
|
24
|
+
here = (start or pathlib.Path.cwd()).resolve()
|
|
25
|
+
for d in [here, *here.parents]:
|
|
26
|
+
if (d / RESULTS_DIR).is_dir():
|
|
27
|
+
return d / RESULTS_DIR
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def require_root() -> pathlib.Path:
|
|
32
|
+
root = find_root()
|
|
33
|
+
if root is None:
|
|
34
|
+
print(f"no {RESULTS_DIR}/ here or above. `results init` makes one.")
|
|
35
|
+
sys.exit(2)
|
|
36
|
+
return root
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ledger_path(root: pathlib.Path) -> pathlib.Path:
|
|
40
|
+
return root / ledger.LEDGER
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_init(a) -> int:
|
|
44
|
+
d = pathlib.Path.cwd() / RESULTS_DIR
|
|
45
|
+
if d.exists():
|
|
46
|
+
print(f"{d} already exists.")
|
|
47
|
+
return 1
|
|
48
|
+
d.mkdir()
|
|
49
|
+
lp = d / ledger.LEDGER
|
|
50
|
+
lp.touch()
|
|
51
|
+
ledger.append_event(lp, {"event": "init"})
|
|
52
|
+
print(f"created {RESULTS_DIR}/")
|
|
53
|
+
print(f" {ledger.LEDGER} append-only event log")
|
|
54
|
+
print("\nseal your inputs before running: `results seal prereg.md script.py data.csv`")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_seal(a) -> int:
|
|
59
|
+
root = require_root()
|
|
60
|
+
lp = ledger_path(root)
|
|
61
|
+
sealed = []
|
|
62
|
+
for name in a.files:
|
|
63
|
+
p = pathlib.Path(name).resolve()
|
|
64
|
+
if not p.is_file():
|
|
65
|
+
print(f"not a file: {name}")
|
|
66
|
+
return 1
|
|
67
|
+
digest = ledger.sha256_of_file(p)
|
|
68
|
+
sealed.append({"path": os.path.relpath(p), "sha256": digest})
|
|
69
|
+
ev = ledger.append_event(lp, {
|
|
70
|
+
"event": "seal",
|
|
71
|
+
"role": a.role,
|
|
72
|
+
"files": sealed,
|
|
73
|
+
})
|
|
74
|
+
print(f"sealed {len(sealed)} file(s) as {a.role}")
|
|
75
|
+
for s in sealed:
|
|
76
|
+
print(f" {s['sha256'][:16]}… {s['path']}")
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_access(a) -> int:
|
|
81
|
+
root = require_root()
|
|
82
|
+
lp = ledger_path(root)
|
|
83
|
+
if a.level not in ACCESS_LEVELS:
|
|
84
|
+
print(f"level must be one of: {', '.join(ACCESS_LEVELS)}")
|
|
85
|
+
return 1
|
|
86
|
+
ev = ledger.append_event(lp, {
|
|
87
|
+
"event": "access",
|
|
88
|
+
"level": a.level,
|
|
89
|
+
"note": a.note,
|
|
90
|
+
})
|
|
91
|
+
print(f"recorded: {a.level} — {a.note}")
|
|
92
|
+
if a.level == "outcomes seen":
|
|
93
|
+
print("\nany analysis registered after this is retrospective, not confirmatory.")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_run(a) -> int:
|
|
98
|
+
root = require_root()
|
|
99
|
+
lp = ledger_path(root)
|
|
100
|
+
existing = ledger.read_ledger(lp)
|
|
101
|
+
existing_ids = {e["run_id"] for e in existing if e.get("event") == "run"}
|
|
102
|
+
if a.run_id in existing_ids:
|
|
103
|
+
print(f"warning: run id '{a.run_id}' already exists in the ledger.")
|
|
104
|
+
print("the new run will be recorded alongside the old one.")
|
|
105
|
+
outputs = []
|
|
106
|
+
for name in a.files:
|
|
107
|
+
p = pathlib.Path(name).resolve()
|
|
108
|
+
if not p.is_file():
|
|
109
|
+
print(f"not a file: {name}")
|
|
110
|
+
return 1
|
|
111
|
+
digest = ledger.sha256_of_file(p)
|
|
112
|
+
outputs.append({"path": os.path.relpath(p), "sha256": digest})
|
|
113
|
+
ev = ledger.append_event(lp, {
|
|
114
|
+
"event": "run",
|
|
115
|
+
"run_id": a.run_id,
|
|
116
|
+
"outputs": outputs,
|
|
117
|
+
"note": a.note or "",
|
|
118
|
+
})
|
|
119
|
+
print(f"run {a.run_id}: {len(outputs)} output(s)")
|
|
120
|
+
for o in outputs:
|
|
121
|
+
print(f" {o['sha256'][:16]}… {o['path']}")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_claim(a) -> int:
|
|
126
|
+
root = require_root()
|
|
127
|
+
lp = ledger_path(root)
|
|
128
|
+
|
|
129
|
+
events = ledger.read_ledger(lp)
|
|
130
|
+
run_ids = {e["run_id"] for e in events if e.get("event") == "run"}
|
|
131
|
+
if a.run_id not in run_ids:
|
|
132
|
+
print(f"no run with id '{a.run_id}' in the ledger.")
|
|
133
|
+
print(f"known runs: {', '.join(sorted(run_ids)) or '(none)'}")
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
ev = ledger.append_event(lp, {
|
|
137
|
+
"event": "claim",
|
|
138
|
+
"claim": a.text,
|
|
139
|
+
"run_id": a.run_id,
|
|
140
|
+
"confirmatory": a.confirmatory,
|
|
141
|
+
"location": a.location or "",
|
|
142
|
+
})
|
|
143
|
+
status = "confirmatory" if a.confirmatory else "exploratory"
|
|
144
|
+
print(f"claim ({status}): {a.text[:72]}")
|
|
145
|
+
print(f" backed by run: {a.run_id}")
|
|
146
|
+
if a.location:
|
|
147
|
+
print(f" appears in: {a.location}")
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_verify(a) -> int:
|
|
152
|
+
root = require_root()
|
|
153
|
+
lp = ledger_path(root)
|
|
154
|
+
|
|
155
|
+
ok, problems = ledger.verify_chain(lp)
|
|
156
|
+
if not ok:
|
|
157
|
+
print("CHAIN BROKEN")
|
|
158
|
+
for p in problems:
|
|
159
|
+
print(f" {p}")
|
|
160
|
+
return 1
|
|
161
|
+
|
|
162
|
+
events = ledger.read_ledger(lp)
|
|
163
|
+
print(f"chain intact: {len(events)} events\n")
|
|
164
|
+
|
|
165
|
+
counts = {}
|
|
166
|
+
for e in events:
|
|
167
|
+
t = e.get("event", "?")
|
|
168
|
+
counts[t] = counts.get(t, 0) + 1
|
|
169
|
+
for t, n in sorted(counts.items()):
|
|
170
|
+
print(f" {t:<12}{n:>5}")
|
|
171
|
+
|
|
172
|
+
drift = 0
|
|
173
|
+
if a.files:
|
|
174
|
+
print("\nfile hashes:")
|
|
175
|
+
file_hashes = {}
|
|
176
|
+
for e in events:
|
|
177
|
+
for f in e.get("files", []) + e.get("outputs", []):
|
|
178
|
+
file_hashes[f["path"]] = f["sha256"]
|
|
179
|
+
for path, expected in sorted(file_hashes.items()):
|
|
180
|
+
p = pathlib.Path(path)
|
|
181
|
+
if not p.exists():
|
|
182
|
+
print(f" MISSING {path}")
|
|
183
|
+
drift += 1
|
|
184
|
+
else:
|
|
185
|
+
actual = ledger.sha256_of_file(p)
|
|
186
|
+
if actual == expected:
|
|
187
|
+
print(f" ok {path}")
|
|
188
|
+
else:
|
|
189
|
+
print(f" CHANGED {path}")
|
|
190
|
+
print(f" sealed {expected[:16]}…")
|
|
191
|
+
print(f" now {actual[:16]}…")
|
|
192
|
+
drift += 1
|
|
193
|
+
if drift:
|
|
194
|
+
print(f"\n{drift} file(s) changed or missing since they were recorded.")
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
access_events = [e for e in events if e.get("event") == "access"]
|
|
198
|
+
if access_events:
|
|
199
|
+
print("\ndata access timeline:")
|
|
200
|
+
for e in access_events:
|
|
201
|
+
print(f" {e['timestamp'][:19]} {e['level']:<20} {e.get('note', '')}")
|
|
202
|
+
|
|
203
|
+
claims = [e for e in events if e.get("event") == "claim"]
|
|
204
|
+
if claims:
|
|
205
|
+
unlinked = []
|
|
206
|
+
for c in claims:
|
|
207
|
+
run_events = [e for e in events
|
|
208
|
+
if e.get("event") == "run" and e.get("run_id") == c.get("run_id")]
|
|
209
|
+
if not run_events:
|
|
210
|
+
unlinked.append(c)
|
|
211
|
+
if unlinked:
|
|
212
|
+
print(f"\n{len(unlinked)} claim(s) reference missing runs:")
|
|
213
|
+
for c in unlinked:
|
|
214
|
+
print(f" {c['claim'][:60]} (run: {c.get('run_id')})")
|
|
215
|
+
|
|
216
|
+
print()
|
|
217
|
+
if ok and not (a.files and drift):
|
|
218
|
+
print("all checks passed.")
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
ACCESS_LEVELS = [
|
|
223
|
+
"nothing seen",
|
|
224
|
+
"metadata only",
|
|
225
|
+
"structure seen",
|
|
226
|
+
"outcomes seen",
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main() -> int:
|
|
231
|
+
ap = argparse.ArgumentParser(prog="results", description=__doc__.split("\n")[0])
|
|
232
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
233
|
+
|
|
234
|
+
sub.add_parser("init", help="start tracking results here")
|
|
235
|
+
|
|
236
|
+
s = sub.add_parser("seal", help="hash inputs before a run")
|
|
237
|
+
s.add_argument("files", nargs="+")
|
|
238
|
+
s.add_argument("--role", default="input",
|
|
239
|
+
help="what these files are: input, prereg, script, data")
|
|
240
|
+
s.set_defaults(fn=cmd_seal)
|
|
241
|
+
|
|
242
|
+
ac = sub.add_parser("access", help="record a data-access event")
|
|
243
|
+
ac.add_argument("note", help="what was accessed and why")
|
|
244
|
+
ac.add_argument("--level", default="metadata only",
|
|
245
|
+
help=f"one of: {', '.join(ACCESS_LEVELS)}")
|
|
246
|
+
ac.set_defaults(fn=cmd_access)
|
|
247
|
+
|
|
248
|
+
r = sub.add_parser("run", help="record outputs after a run")
|
|
249
|
+
r.add_argument("files", nargs="+")
|
|
250
|
+
r.add_argument("--run-id", required=True, help="a name for this run")
|
|
251
|
+
r.add_argument("--note", help="what this run computed")
|
|
252
|
+
r.set_defaults(fn=cmd_run)
|
|
253
|
+
|
|
254
|
+
cl = sub.add_parser("claim", help="bind a manuscript claim to a run")
|
|
255
|
+
cl.add_argument("text", help="the claim, as it appears in the manuscript")
|
|
256
|
+
cl.add_argument("--run-id", required=True, help="which run backs this claim")
|
|
257
|
+
cl.add_argument("--confirmatory", action="store_true",
|
|
258
|
+
help="mark as confirmatory (default: exploratory)")
|
|
259
|
+
cl.add_argument("--location", help="where in the manuscript: Table 2, Section 4.1, etc.")
|
|
260
|
+
cl.set_defaults(fn=cmd_claim)
|
|
261
|
+
|
|
262
|
+
v = sub.add_parser("verify", help="check the ledger and every hash it names")
|
|
263
|
+
v.add_argument("--files", action="store_true",
|
|
264
|
+
help="also check that sealed/output files still match their hashes")
|
|
265
|
+
v.set_defaults(fn=cmd_verify)
|
|
266
|
+
|
|
267
|
+
a = ap.parse_args()
|
|
268
|
+
if not a.cmd:
|
|
269
|
+
ap.print_help()
|
|
270
|
+
return 0
|
|
271
|
+
if a.cmd == "init":
|
|
272
|
+
return cmd_init(a)
|
|
273
|
+
return a.fn(a)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
sys.exit(main())
|
results/ledger.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Append-only JSONL ledger: every event is one line, hash-chained to the previous."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
LEDGER = "ledger.jsonl"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sha256_of_file(path: pathlib.Path) -> str:
|
|
14
|
+
h = hashlib.sha256()
|
|
15
|
+
with open(path, "rb") as f:
|
|
16
|
+
for chunk in iter(lambda: f.read(1 << 16), b""):
|
|
17
|
+
h.update(chunk)
|
|
18
|
+
return h.hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sha256_of_str(s: str) -> str:
|
|
22
|
+
return hashlib.sha256(s.encode()).hexdigest()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def now_iso() -> str:
|
|
26
|
+
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def last_hash(ledger: pathlib.Path) -> str:
|
|
30
|
+
"""The hash of the last line as stored, or a zero hash if the ledger is empty."""
|
|
31
|
+
if not ledger.exists() or ledger.stat().st_size == 0:
|
|
32
|
+
return "0" * 64
|
|
33
|
+
with open(ledger, "rb") as f:
|
|
34
|
+
last = b""
|
|
35
|
+
for line in f:
|
|
36
|
+
if line.strip():
|
|
37
|
+
last = line
|
|
38
|
+
return sha256_of_str(last.decode().strip())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def append_event(ledger: pathlib.Path, event: dict) -> dict:
|
|
42
|
+
"""Write one event to the ledger. Returns the event with chain fields added."""
|
|
43
|
+
event["timestamp"] = now_iso()
|
|
44
|
+
event["prev_hash"] = last_hash(ledger)
|
|
45
|
+
line = json.dumps(event, separators=(",", ":"), sort_keys=True)
|
|
46
|
+
with open(ledger, "a") as f:
|
|
47
|
+
f.write(line + "\n")
|
|
48
|
+
return event
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_ledger(ledger: pathlib.Path) -> list[dict]:
|
|
52
|
+
if not ledger.exists():
|
|
53
|
+
return []
|
|
54
|
+
events = []
|
|
55
|
+
for line in ledger.read_text().splitlines():
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if line:
|
|
58
|
+
events.append(json.loads(line))
|
|
59
|
+
return events
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def verify_chain(ledger: pathlib.Path) -> tuple[bool, list[str]]:
|
|
63
|
+
"""Check that every line's prev_hash matches the hash of the previous line as stored."""
|
|
64
|
+
if not ledger.exists():
|
|
65
|
+
return True, []
|
|
66
|
+
lines = [ln.strip() for ln in ledger.read_text().splitlines() if ln.strip()]
|
|
67
|
+
if not lines:
|
|
68
|
+
return True, []
|
|
69
|
+
problems = []
|
|
70
|
+
prev = "0" * 64
|
|
71
|
+
for i, raw in enumerate(lines):
|
|
72
|
+
ev = json.loads(raw)
|
|
73
|
+
if ev.get("prev_hash") != prev:
|
|
74
|
+
problems.append(
|
|
75
|
+
f"line {i + 1}: prev_hash mismatch — expected {prev[:16]}…, "
|
|
76
|
+
f"got {ev.get('prev_hash', '???')[:16]}…")
|
|
77
|
+
prev = sha256_of_str(raw)
|
|
78
|
+
return len(problems) == 0, problems
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: results-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Seal a run, record what it produced, and verify the chain
|
|
5
|
+
Author-email: Elliot Tower <elliot@elliottower.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: open science,provenance,reproducibility,results
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# results
|
|
13
|
+
|
|
14
|
+
Seal a run, record what it produced, and verify the chain.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install results-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
results init
|
|
26
|
+
results seal prereg.md analysis.py data.csv --role input
|
|
27
|
+
results access "read zenodo metadata" --level "metadata only"
|
|
28
|
+
|
|
29
|
+
# run the computation, then record its outputs
|
|
30
|
+
results run output.json --run-id exp_001 --note "ICC analysis"
|
|
31
|
+
results claim "ICC = 0.42" --run-id exp_001 --confirmatory --location "Table 2"
|
|
32
|
+
results verify --files
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
chain intact: 5 events
|
|
37
|
+
|
|
38
|
+
access 1
|
|
39
|
+
claim 1
|
|
40
|
+
init 1
|
|
41
|
+
run 1
|
|
42
|
+
seal 1
|
|
43
|
+
|
|
44
|
+
file hashes:
|
|
45
|
+
ok prereg.md
|
|
46
|
+
ok analysis.py
|
|
47
|
+
ok data.csv
|
|
48
|
+
ok output.json
|
|
49
|
+
|
|
50
|
+
all checks passed.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Command | What it does |
|
|
56
|
+
|---------|-------------|
|
|
57
|
+
| `results init` | Start tracking results here |
|
|
58
|
+
| `results seal <file>...` | Hash inputs before a run |
|
|
59
|
+
| `results access <note>` | Record a data-access event |
|
|
60
|
+
| `results run <file>...` | Record outputs after a run |
|
|
61
|
+
| `results claim <text>` | Bind a manuscript claim to a run |
|
|
62
|
+
| `results verify` | Check the ledger chain and every hash it names |
|
|
63
|
+
|
|
64
|
+
## The chain
|
|
65
|
+
|
|
66
|
+
A number in a manuscript names a claim. The claim names a run. The run names its outputs. The
|
|
67
|
+
outputs were hashed when they were recorded. The inputs were hashed before the run started.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
manuscript → claim → run → output file → sha256
|
|
71
|
+
input files → sha256
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`results verify --files` walks the whole thing and tells you what moved.
|
|
75
|
+
|
|
76
|
+
## Data-access levels
|
|
77
|
+
|
|
78
|
+
The access timeline is what makes the confirmatory/exploratory distinction verifiable.
|
|
79
|
+
|
|
80
|
+
| Level | Meaning |
|
|
81
|
+
|-------|---------|
|
|
82
|
+
| `nothing seen` | No target data touched |
|
|
83
|
+
| `metadata only` | Structure, region names, sample sizes — not outcomes |
|
|
84
|
+
| `structure seen` | Data shape and distributions, not the target variable |
|
|
85
|
+
| `outcomes seen` | The dependent variable was observed |
|
|
86
|
+
|
|
87
|
+
An analysis registered after `outcomes seen` is retrospective.
|
|
88
|
+
|
|
89
|
+
## Verify output
|
|
90
|
+
|
|
91
|
+
| Result | Meaning |
|
|
92
|
+
|--------|---------|
|
|
93
|
+
| `chain intact` | Every event's prev_hash matches the line before it |
|
|
94
|
+
| `CHAIN BROKEN` | The ledger was edited after it was written |
|
|
95
|
+
| `ok` | File matches its recorded hash |
|
|
96
|
+
| `CHANGED` | File was modified since it was recorded |
|
|
97
|
+
| `MISSING` | File no longer exists |
|
|
98
|
+
|
|
99
|
+
## The ledger
|
|
100
|
+
|
|
101
|
+
Append-only JSONL in `.results/ledger.jsonl`. Each line is hash-chained to the previous — editing
|
|
102
|
+
or inserting a line breaks the chain. `git diff` shows what changed; `results verify` checks
|
|
103
|
+
whether it should have.
|
|
104
|
+
|
|
105
|
+
## Claude Code
|
|
106
|
+
|
|
107
|
+
`plugin/` is a Claude Code plugin that tells Claude when to reach for the CLI.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
/plugin marketplace add elliottower/results
|
|
111
|
+
/plugin install results@results
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
MIT licensed.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
results/__init__.py,sha256=quPt6si4TzPx3vsWIdqxTKZy9hnQYlWqEb5Q66t_3No,122
|
|
2
|
+
results/cli.py,sha256=H8cGnttpljtrHmti01dcnd-Ct4DzR3TwDfXr3MovVOA,8919
|
|
3
|
+
results/ledger.py,sha256=apzy1v6NGOp5Uy5hW51xBk_6VtIEoaJ-V-rwJU9YD2M,2366
|
|
4
|
+
results_cli-0.1.0.dist-info/METADATA,sha256=lJBuE-UMNqpAvIxMv1xxQjxWBUUkCMuuMW0lVPpZkBw,3135
|
|
5
|
+
results_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
6
|
+
results_cli-0.1.0.dist-info/entry_points.txt,sha256=IKJM5z5zKmfR1JLm5wze8zXDYnbOrpp1SKg8JQ3Ifsk,45
|
|
7
|
+
results_cli-0.1.0.dist-info/licenses/LICENSE,sha256=-C2xzeJxZAl5nBr-WFxtiYY3521mIQjxI0-_5GEpFCQ,1069
|
|
8
|
+
results_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elliot Tower
|
|
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.
|