flywire-coding-cortex 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.
- flywire_coding_cortex/__init__.py +3 -0
- flywire_coding_cortex/assets/circuit.seed.json +1 -0
- flywire_coding_cortex/assets/manifest.json +54 -0
- flywire_coding_cortex/assets/seed-graph.json +18 -0
- flywire_coding_cortex/assets/skill/SKILL.md +55 -0
- flywire_coding_cortex/assets/skill/practices.md +14 -0
- flywire_coding_cortex/assets/skill/roles.md +17 -0
- flywire_coding_cortex/bridge.py +85 -0
- flywire_coding_cortex/cli.py +304 -0
- flywire_coding_cortex/etl/__init__.py +1 -0
- flywire_coding_cortex/etl/build_circuit.py +210 -0
- flywire_coding_cortex/etl/download.py +128 -0
- flywire_coding_cortex/install_skills.py +74 -0
- flywire_coding_cortex/lif.py +335 -0
- flywire_coding_cortex/mcp_server.py +208 -0
- flywire_coding_cortex/memory.py +106 -0
- flywire_coding_cortex/paths.py +112 -0
- flywire_coding_cortex/sense.py +62 -0
- flywire_coding_cortex-0.1.0.dist-info/METADATA +447 -0
- flywire_coding_cortex-0.1.0.dist-info/RECORD +24 -0
- flywire_coding_cortex-0.1.0.dist-info/WHEEL +5 -0
- flywire_coding_cortex-0.1.0.dist-info/entry_points.txt +2 -0
- flywire_coding_cortex-0.1.0.dist-info/licenses/LICENSE +21 -0
- flywire_coding_cortex-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Build curated circuit.json from Codex CSVs or Zenodo Feathers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import csv
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
from collections import Counter, defaultdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
CORE_TYPES = {
|
|
12
|
+
"LC4": "lc4",
|
|
13
|
+
"LPLC2": "lplc2",
|
|
14
|
+
"DNp01": "gf",
|
|
15
|
+
"DNa02": "dna02",
|
|
16
|
+
"DNa01": "dna01",
|
|
17
|
+
"DNp09": "dnp09",
|
|
18
|
+
"DNg11": "dng11",
|
|
19
|
+
"MDN": "mdn",
|
|
20
|
+
"DNp02": "escw",
|
|
21
|
+
"DNp04": "escw",
|
|
22
|
+
"DNp11": "escw",
|
|
23
|
+
}
|
|
24
|
+
NT_SIGN = {"ACH": 1.0, "GABA": -1.0, "GLUT": -1.0, "DA": 0.5, "SER": 0.5, "OCT": 0.5}
|
|
25
|
+
MAX_PARTNERS = 330
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_from_codex_dir(raw: Path, out: Path) -> dict[str, Any]:
|
|
29
|
+
def rows(name: str):
|
|
30
|
+
with gzip.open(raw / name, "rt", encoding="utf-8", errors="replace") as f:
|
|
31
|
+
r = csv.reader(f)
|
|
32
|
+
next(r)
|
|
33
|
+
yield from r
|
|
34
|
+
|
|
35
|
+
core: dict[str, str] = {}
|
|
36
|
+
type_of: dict[str, str] = {}
|
|
37
|
+
counts: dict[str, int] = defaultdict(int)
|
|
38
|
+
for row in rows("consolidated_cell_types.csv.gz"):
|
|
39
|
+
rid, ptype = row[0], row[1].strip()
|
|
40
|
+
role = CORE_TYPES.get(ptype)
|
|
41
|
+
if role:
|
|
42
|
+
core[rid] = role
|
|
43
|
+
type_of[rid] = ptype
|
|
44
|
+
counts[ptype] += 1
|
|
45
|
+
print("core populations:", dict(counts))
|
|
46
|
+
if not counts.get("LC4") or not counts.get("LPLC2") or not counts.get("DNp01"):
|
|
47
|
+
raise SystemExit("FATAL: missing a core population — check type names")
|
|
48
|
+
|
|
49
|
+
klass: dict[str, tuple[str, str]] = {}
|
|
50
|
+
for row in rows("classification.csv.gz"):
|
|
51
|
+
klass[row[0]] = (row[2], row[6])
|
|
52
|
+
|
|
53
|
+
pos: dict[str, tuple[float, float, float]] = {}
|
|
54
|
+
for row in rows("coordinates.csv.gz"):
|
|
55
|
+
rid = row[0]
|
|
56
|
+
if rid in pos:
|
|
57
|
+
continue
|
|
58
|
+
p = row[1].strip("[]").split()
|
|
59
|
+
if len(p) == 3:
|
|
60
|
+
pos[rid] = (float(p[0]), float(p[1]), float(p[2]))
|
|
61
|
+
|
|
62
|
+
partner_strength: dict[str, int] = defaultdict(int)
|
|
63
|
+
strength_by_role: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
|
64
|
+
for row in rows("connections.csv.gz"):
|
|
65
|
+
pre, post, syn = row[0], row[1], int(row[3])
|
|
66
|
+
pre_core, post_core = pre in core, post in core
|
|
67
|
+
if pre_core and not post_core:
|
|
68
|
+
partner_strength[post] += syn
|
|
69
|
+
strength_by_role[core[pre]][post] += syn
|
|
70
|
+
elif post_core and not pre_core:
|
|
71
|
+
partner_strength[pre] += syn
|
|
72
|
+
strength_by_role[core[post]][pre] += syn
|
|
73
|
+
|
|
74
|
+
usable = lambda r: r in pos and r in klass
|
|
75
|
+
ranked = [rid for rid, _ in sorted(partner_strength.items(), key=lambda kv: -kv[1]) if usable(rid)]
|
|
76
|
+
partners: list[str] = []
|
|
77
|
+
seen: set[str] = set()
|
|
78
|
+
|
|
79
|
+
def take(cands: list[str], k: int) -> None:
|
|
80
|
+
n = 0
|
|
81
|
+
for r in cands:
|
|
82
|
+
if r in seen or not usable(r):
|
|
83
|
+
continue
|
|
84
|
+
seen.add(r)
|
|
85
|
+
partners.append(r)
|
|
86
|
+
n += 1
|
|
87
|
+
if n == k:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
for role in ("gf", "dna01", "dna02", "dnp09", "dng11", "mdn", "escw"):
|
|
91
|
+
take([r for r, _ in sorted(strength_by_role[role].items(), key=lambda kv: -kv[1])], 10)
|
|
92
|
+
take([r for r in ranked if klass[r][0] == "ascending"], 24)
|
|
93
|
+
take([r for r in ranked if klass[r][0] == "sensory"], 16)
|
|
94
|
+
take(ranked, MAX_PARTNERS - len(partners))
|
|
95
|
+
print("partner super_classes:", dict(Counter(klass[r][0] for r in partners)))
|
|
96
|
+
|
|
97
|
+
members = list(core.keys()) + partners
|
|
98
|
+
member_idx = {rid: i for i, rid in enumerate(members)}
|
|
99
|
+
edges: list[list[float]] = []
|
|
100
|
+
nt_missing = 0
|
|
101
|
+
for row in rows("connections.csv.gz"):
|
|
102
|
+
pre, post = row[0], row[1]
|
|
103
|
+
i, j = member_idx.get(pre), member_idx.get(post)
|
|
104
|
+
if i is None or j is None:
|
|
105
|
+
continue
|
|
106
|
+
syn, nt = int(row[3]), row[4].strip().upper()
|
|
107
|
+
sign = NT_SIGN.get(nt)
|
|
108
|
+
if sign is None:
|
|
109
|
+
sign, nt_missing = 1.0, nt_missing + 1
|
|
110
|
+
edges.append([i, j, round(syn * sign, 1)])
|
|
111
|
+
print(f"circuit edges: {len(edges)} (unknown nt on {nt_missing})")
|
|
112
|
+
|
|
113
|
+
xs = [p[0] for p in pos.values()]
|
|
114
|
+
ys = [p[1] for p in pos.values()]
|
|
115
|
+
zs = [p[2] for p in pos.values()]
|
|
116
|
+
cx, cy, cz = (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2, (min(zs) + max(zs)) / 2
|
|
117
|
+
scale = 20.0 / max(max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs))
|
|
118
|
+
|
|
119
|
+
def norm(p: tuple[float, float, float]) -> list[float]:
|
|
120
|
+
return [
|
|
121
|
+
round((p[0] - cx) * scale, 3),
|
|
122
|
+
round(-(p[1] - cy) * scale, 3),
|
|
123
|
+
round(-(p[2] - cz) * scale, 3),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
neurons = []
|
|
127
|
+
for rid in members:
|
|
128
|
+
sc, side = klass.get(rid, ("", ""))
|
|
129
|
+
p = norm(pos[rid]) if rid in pos else [0.0, 0.0, 0.0]
|
|
130
|
+
neurons.append(
|
|
131
|
+
{
|
|
132
|
+
"id": rid,
|
|
133
|
+
"type": type_of.get(rid, sc or "?"),
|
|
134
|
+
"role": core.get(rid, "other"),
|
|
135
|
+
"side": side,
|
|
136
|
+
"pos": p,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
circuit = {
|
|
140
|
+
"neurons": neurons,
|
|
141
|
+
"edges": edges,
|
|
142
|
+
"source": "FlyWire Codex FAFB v783 connections.csv (syn>=5, signed by nt_type)",
|
|
143
|
+
}
|
|
144
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
out.write_text(json.dumps(circuit, separators=(",", ":")) + "\n", encoding="utf-8")
|
|
146
|
+
print(f"Wrote {out}: {len(neurons)} neurons, {len(edges)} edges")
|
|
147
|
+
return circuit
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_from_feather_dir(raw: Path, out: Path) -> dict[str, Any]:
|
|
151
|
+
"""Build a curated circuit using proofread_connections Feather + cell types if present.
|
|
152
|
+
|
|
153
|
+
Falls back to requiring Codex cell-type CSVs in the same raw dir (downloaded
|
|
154
|
+
automatically alongside Feathers when missing).
|
|
155
|
+
"""
|
|
156
|
+
import pandas as pd
|
|
157
|
+
|
|
158
|
+
conn_path = raw / "proofread_connections_783.feather"
|
|
159
|
+
if not conn_path.exists():
|
|
160
|
+
raise FileNotFoundError(f"Missing {conn_path}")
|
|
161
|
+
|
|
162
|
+
# Need cell types — pull Codex types if absent
|
|
163
|
+
types_gz = raw / "consolidated_cell_types.csv.gz"
|
|
164
|
+
class_gz = raw / "classification.csv.gz"
|
|
165
|
+
coords_gz = raw / "coordinates.csv.gz"
|
|
166
|
+
if not types_gz.exists():
|
|
167
|
+
from .download import download_file, load_manifest
|
|
168
|
+
|
|
169
|
+
base = load_manifest()["profiles"]["codex"]["base_url"].rstrip("/")
|
|
170
|
+
for name in (
|
|
171
|
+
"consolidated_cell_types.csv.gz",
|
|
172
|
+
"classification.csv.gz",
|
|
173
|
+
"coordinates.csv.gz",
|
|
174
|
+
"connections.csv.gz",
|
|
175
|
+
):
|
|
176
|
+
dest = raw / name
|
|
177
|
+
if not dest.exists():
|
|
178
|
+
download_file(f"{base}/{name}", dest, label=name)
|
|
179
|
+
|
|
180
|
+
# Prefer Codex thresholded connections for role ETL consistency when present;
|
|
181
|
+
# otherwise synthesize a connections.csv.gz-like stream from Feather.
|
|
182
|
+
if (raw / "connections.csv.gz").exists():
|
|
183
|
+
return build_from_codex_dir(raw, out)
|
|
184
|
+
|
|
185
|
+
df = pd.read_feather(conn_path)
|
|
186
|
+
# Normalize column names across releases
|
|
187
|
+
cols = {c.lower(): c for c in df.columns}
|
|
188
|
+
pre_c = cols.get("pre_root_id") or cols.get("pre") or cols.get("from") or list(df.columns)[0]
|
|
189
|
+
post_c = cols.get("post_root_id") or cols.get("post") or cols.get("to") or list(df.columns)[1]
|
|
190
|
+
syn_c = cols.get("syn_count") or cols.get("synapse_count") or cols.get("count")
|
|
191
|
+
if syn_c is None:
|
|
192
|
+
for c in df.columns:
|
|
193
|
+
if "syn" in c.lower() or "count" in c.lower():
|
|
194
|
+
syn_c = c
|
|
195
|
+
break
|
|
196
|
+
if syn_c is None:
|
|
197
|
+
raise SystemExit(f"Cannot find synapse count column in {list(df.columns)}")
|
|
198
|
+
|
|
199
|
+
tmp_conn = raw / "connections.csv.gz"
|
|
200
|
+
subset = df[[pre_c, post_c, syn_c]].copy()
|
|
201
|
+
subset.columns = ["pre", "post", "syn"]
|
|
202
|
+
with gzip.open(tmp_conn, "wt", encoding="utf-8") as f:
|
|
203
|
+
w = csv.writer(f)
|
|
204
|
+
w.writerow(["pre_root_id", "post_root_id", "neuropil", "syn_count", "nt_type"])
|
|
205
|
+
for pre, post, syn in subset.itertuples(index=False, name=None):
|
|
206
|
+
syn_i = int(syn)
|
|
207
|
+
if syn_i < 5:
|
|
208
|
+
continue
|
|
209
|
+
w.writerow([str(pre), str(post), "", syn_i, "ACH"])
|
|
210
|
+
return build_from_codex_dir(raw, out)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Download profile assets into the user cache."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import urllib.request
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..paths import bundled_manifest, bundled_seed_circuit, profile_dir
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_manifest() -> dict[str, Any]:
|
|
14
|
+
return json.loads(bundled_manifest().read_text(encoding="utf-8"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_profiles() -> dict[str, Any]:
|
|
18
|
+
return load_manifest()["profiles"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def download_file(url: str, dest: Path, *, label: str = "") -> None:
|
|
22
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
tmp = dest.with_suffix(dest.suffix + ".part")
|
|
24
|
+
print(f"Downloading {label or dest.name} …")
|
|
25
|
+
urllib.request.urlretrieve(url, tmp) # noqa: S310 — curated manifest URLs
|
|
26
|
+
tmp.replace(dest)
|
|
27
|
+
print(f" -> {dest} ({dest.stat().st_size:,} bytes)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fetch_seed(*, force: bool = False) -> Path:
|
|
31
|
+
dest = profile_dir("seed") / "circuit.json"
|
|
32
|
+
if dest.exists() and not force:
|
|
33
|
+
return dest
|
|
34
|
+
seed = bundled_seed_circuit()
|
|
35
|
+
if seed is None:
|
|
36
|
+
raise FileNotFoundError(
|
|
37
|
+
"circuit.seed.json missing. Build it with: flywire-cortex fetch --profile codex"
|
|
38
|
+
)
|
|
39
|
+
shutil.copy2(seed, dest)
|
|
40
|
+
meta = {
|
|
41
|
+
"profile": "seed",
|
|
42
|
+
"source": "bundled circuit.seed.json",
|
|
43
|
+
"neurons": _count_neurons(dest),
|
|
44
|
+
}
|
|
45
|
+
(profile_dir("seed") / "meta.json").write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
|
46
|
+
return dest
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fetch_codex(*, force: bool = False) -> Path:
|
|
50
|
+
from .build_circuit import build_from_codex_dir
|
|
51
|
+
|
|
52
|
+
raw = profile_dir("codex") / "raw"
|
|
53
|
+
circuit = profile_dir("codex") / "circuit.json"
|
|
54
|
+
if circuit.exists() and not force:
|
|
55
|
+
return circuit
|
|
56
|
+
manifest = load_manifest()["profiles"]["codex"]
|
|
57
|
+
base = manifest["base_url"].rstrip("/")
|
|
58
|
+
raw.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
for f in manifest["files"]:
|
|
60
|
+
name = f["name"]
|
|
61
|
+
dest = raw / name
|
|
62
|
+
if dest.exists() and not force:
|
|
63
|
+
continue
|
|
64
|
+
download_file(f"{base}/{f['path']}", dest, label=name)
|
|
65
|
+
build_from_codex_dir(raw, circuit)
|
|
66
|
+
meta = {
|
|
67
|
+
"profile": "codex",
|
|
68
|
+
"source": base,
|
|
69
|
+
"neurons": _count_neurons(circuit),
|
|
70
|
+
}
|
|
71
|
+
(profile_dir("codex") / "meta.json").write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
|
72
|
+
# also refresh bundled seed for packaging workflows
|
|
73
|
+
seed_out = bundled_seed_circuit()
|
|
74
|
+
if seed_out is None:
|
|
75
|
+
seed_out = Path(__file__).resolve().parents[3] / "data" / "circuit.seed.json"
|
|
76
|
+
seed_out.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
shutil.copy2(circuit, seed_out)
|
|
78
|
+
assets = Path(__file__).resolve().parents[1] / "assets" / "circuit.seed.json"
|
|
79
|
+
assets.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
shutil.copy2(circuit, assets)
|
|
81
|
+
return circuit
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def fetch_full(*, force: bool = False, include_synapses: bool = False, yes: bool = False) -> Path:
|
|
85
|
+
if not yes:
|
|
86
|
+
raise SystemExit(
|
|
87
|
+
"Profile 'full' downloads ~10.6 GB. Re-run with --yes to confirm:\n"
|
|
88
|
+
" flywire-cortex fetch --profile full --yes\n"
|
|
89
|
+
"Optional largest synapse table: add --include-synapses"
|
|
90
|
+
)
|
|
91
|
+
try:
|
|
92
|
+
import pandas as pd # noqa: F401
|
|
93
|
+
import pyarrow # noqa: F401
|
|
94
|
+
except ImportError as exc:
|
|
95
|
+
raise SystemExit(
|
|
96
|
+
"Profile 'full' needs: pip install 'flywire-coding-cortex[full]'"
|
|
97
|
+
) from exc
|
|
98
|
+
|
|
99
|
+
from .build_circuit import build_from_feather_dir
|
|
100
|
+
|
|
101
|
+
raw = profile_dir("full") / "raw"
|
|
102
|
+
circuit = profile_dir("full") / "circuit.json"
|
|
103
|
+
if circuit.exists() and not force:
|
|
104
|
+
return circuit
|
|
105
|
+
manifest = load_manifest()["profiles"]["full"]
|
|
106
|
+
raw.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
for f in manifest["files"]:
|
|
108
|
+
if f.get("optional") and not include_synapses:
|
|
109
|
+
print(f"Skipping optional {f['name']} (pass --include-synapses to fetch)")
|
|
110
|
+
continue
|
|
111
|
+
dest = raw / f["name"]
|
|
112
|
+
if dest.exists() and not force:
|
|
113
|
+
continue
|
|
114
|
+
download_file(f["url"], dest, label=f"{f['name']} (~{f.get('approx_bytes', 0):,} B)")
|
|
115
|
+
build_from_feather_dir(raw, circuit)
|
|
116
|
+
meta = {
|
|
117
|
+
"profile": "full",
|
|
118
|
+
"source": manifest.get("record_url"),
|
|
119
|
+
"neurons": _count_neurons(circuit),
|
|
120
|
+
"include_synapses": include_synapses,
|
|
121
|
+
}
|
|
122
|
+
(profile_dir("full") / "meta.json").write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
|
123
|
+
return circuit
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _count_neurons(path: Path) -> int:
|
|
127
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
128
|
+
return len(data.get("neurons", []))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Copy bundled skill into Cursor / OpenClaw / Claude skill directories."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .paths import bundled_skill_dir, home
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _cursor_skills() -> Path:
|
|
13
|
+
return Path.home() / ".cursor" / "skills" / "flywire-coding-cortex"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _openclaw_skills() -> list[Path]:
|
|
17
|
+
h = Path.home()
|
|
18
|
+
return [
|
|
19
|
+
h / ".openclaw" / "workspace" / "skills" / "flywire-coding-cortex",
|
|
20
|
+
h / ".openclaw" / "skills" / "flywire-coding-cortex",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _claude_skills() -> Path:
|
|
25
|
+
return Path.home() / ".claude" / "skills" / "flywire-coding-cortex"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _copy_skill(dest: Path) -> str:
|
|
29
|
+
src = bundled_skill_dir()
|
|
30
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
if dest.exists():
|
|
32
|
+
shutil.rmtree(dest)
|
|
33
|
+
shutil.copytree(src, dest)
|
|
34
|
+
return str(dest)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def mcp_snippet() -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"flywire-coding-cortex": {
|
|
41
|
+
"command": "flywire-cortex",
|
|
42
|
+
"args": ["mcp"],
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def install_all(targets: list[str] | None = None) -> dict[str, Any]:
|
|
49
|
+
targets = targets or ["cursor", "openclaw", "claude"]
|
|
50
|
+
result: dict[str, Any] = {"installed": [], "skipped": [], "mcp": mcp_snippet(), "home": str(home())}
|
|
51
|
+
if "cursor" in targets:
|
|
52
|
+
result["installed"].append({"target": "cursor", "path": _copy_skill(_cursor_skills())})
|
|
53
|
+
if "openclaw" in targets:
|
|
54
|
+
# prefer workspace path; also try top-level skills
|
|
55
|
+
paths = _openclaw_skills()
|
|
56
|
+
primary = paths[0]
|
|
57
|
+
result["installed"].append({"target": "openclaw", "path": _copy_skill(primary)})
|
|
58
|
+
# second path as optional mirror if parent exists
|
|
59
|
+
if paths[1].parent.exists() or paths[1].parent.parent.exists():
|
|
60
|
+
try:
|
|
61
|
+
paths[1].parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
result["installed"].append({"target": "openclaw-alt", "path": _copy_skill(paths[1])})
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
result["skipped"].append({"target": "openclaw-alt", "error": str(exc)})
|
|
65
|
+
if "claude" in targets:
|
|
66
|
+
result["installed"].append({"target": "claude", "path": _copy_skill(_claude_skills())})
|
|
67
|
+
tip = home() / "mcp.snippet.json"
|
|
68
|
+
tip.write_text(json.dumps(mcp_snippet(), indent=2) + "\n", encoding="utf-8")
|
|
69
|
+
result["mcp_snippet_file"] = str(tip)
|
|
70
|
+
result["hint"] = (
|
|
71
|
+
"Add the mcp snippet to Cursor MCP settings, then restart the agent session. "
|
|
72
|
+
"Ensure `flywire-cortex` is on PATH (pip/uv install)."
|
|
73
|
+
)
|
|
74
|
+
return result
|