@andespindola/crow-skills 0.38.1 → 0.38.2
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.
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/mcp.py +50 -1
- package/package.json +1 -1
- package/release.json +6 -6
- package/skills/crow-uny-mcp/scripts/crow_mcp.py +69 -0
package/crow_skills/__init__.py
CHANGED
package/crow_skills/mcp.py
CHANGED
|
@@ -534,15 +534,46 @@ def _identity(pid):
|
|
|
534
534
|
return None
|
|
535
535
|
|
|
536
536
|
|
|
537
|
+
_NAMES_READER = None
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _served_names():
|
|
541
|
+
"""pid -> server name, borrowed from the runtime module.
|
|
542
|
+
|
|
543
|
+
This module deliberately spawns nothing; it reaches the process table the
|
|
544
|
+
same way it reaches process identity, through crow_mcp.py.
|
|
545
|
+
"""
|
|
546
|
+
global _NAMES_READER
|
|
547
|
+
try:
|
|
548
|
+
if _NAMES_READER is None:
|
|
549
|
+
spec = importlib.util.spec_from_file_location("crow_mcp_status_names", _runtime())
|
|
550
|
+
module = importlib.util.module_from_spec(spec)
|
|
551
|
+
spec.loader.exec_module(module)
|
|
552
|
+
_NAMES_READER = module.serving_names
|
|
553
|
+
return _NAMES_READER() or {}
|
|
554
|
+
except (OSError, ValueError, ImportError, AttributeError):
|
|
555
|
+
return {}
|
|
556
|
+
|
|
557
|
+
|
|
537
558
|
def status(home):
|
|
538
559
|
home = _home(home)
|
|
539
560
|
result = plan(home)
|
|
540
561
|
result.update(action="status", configuredManaged=sum(r["managed"] for r in result["servers"]))
|
|
541
562
|
snapshots = []
|
|
542
563
|
directory = _state(home) / "instances"
|
|
564
|
+
served = _served_names()
|
|
565
|
+
stale = 0
|
|
543
566
|
if directory.exists():
|
|
544
567
|
_safe(directory, home)
|
|
545
|
-
|
|
568
|
+
# Newest first. This used to be sorted(glob)[:200], i.e. by FILENAME —
|
|
569
|
+
# and filenames start with a pid, so the window was arbitrary. Measured
|
|
570
|
+
# on a real machine: 1402 records on disk, the 200 read held 142
|
|
571
|
+
# "stopped" and exactly 2 live pids, while `ps` showed 25 live servers.
|
|
572
|
+
# The observations described mostly dead processes and missed the ones
|
|
573
|
+
# actually running, which is worse than reporting nothing.
|
|
574
|
+
files = sorted(directory.glob("runtime-*.json"), key=lambda f: f.stat().st_mtime, reverse=True)
|
|
575
|
+
stale = max(0, len(files) - 200)
|
|
576
|
+
for path in files[:200]:
|
|
546
577
|
try:
|
|
547
578
|
data = json.loads(_read(path, home))
|
|
548
579
|
# Registry entries are observations, never authority to signal PIDs.
|
|
@@ -561,10 +592,28 @@ def status(home):
|
|
|
561
592
|
row[live_key] = current is not None
|
|
562
593
|
row[verified_key] = current is not None and isinstance(recorded, (tuple, list)) and tuple(recorded) == tuple(current)
|
|
563
594
|
row["source"] = "runtime-reported"
|
|
595
|
+
# Which server this is, when its process is still around.
|
|
596
|
+
name = served.get(data.get("pid"))
|
|
597
|
+
if name:
|
|
598
|
+
row["name"] = name
|
|
564
599
|
snapshots.append(row)
|
|
565
600
|
except (OSError, ValueError):
|
|
566
601
|
continue
|
|
567
602
|
result["runtimeObservations"] = snapshots
|
|
603
|
+
# Aggregates, so a caller can say "N of M alive" instead of counting
|
|
604
|
+
# configured entries and calling it a measurement.
|
|
605
|
+
alive = sorted({r["name"] for r in snapshots if r.get("name") and r.get("pidAlive")})
|
|
606
|
+
result["aliveManaged"] = len(alive)
|
|
607
|
+
result["aliveNames"] = alive
|
|
608
|
+
result["staleRuntimeRecords"] = stale
|
|
609
|
+
# Warm-up caches are reported, never deleted here: dropping one forces a
|
|
610
|
+
# cold start, which is a behaviour change and the owner's decision.
|
|
611
|
+
try:
|
|
612
|
+
caches = list(directory.glob("metadata-*.json")) if directory.exists() else []
|
|
613
|
+
result["warmupCaches"] = len(caches)
|
|
614
|
+
result["warmupCacheBytes"] = sum(c.stat().st_size for c in caches)
|
|
615
|
+
except OSError:
|
|
616
|
+
pass
|
|
568
617
|
result["liveSessionsReloaded"] = False
|
|
569
618
|
return result
|
|
570
619
|
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"crow_skills/__init__.py": {
|
|
29
29
|
"executable": false,
|
|
30
30
|
"kind": "file",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "7765d89d0c3b7dd8b7d765a61e6a507429a1b2f7710fae47b1d014183fb87e40"
|
|
32
32
|
},
|
|
33
33
|
"crow_skills/__main__.py": {
|
|
34
34
|
"executable": false,
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"crow_skills/mcp.py": {
|
|
94
94
|
"executable": false,
|
|
95
95
|
"kind": "file",
|
|
96
|
-
"sha256": "
|
|
96
|
+
"sha256": "cf88b7c1fa188eda74c43e6da17198b7b285cc97706d2003ddf7fbe6d8dd5c99"
|
|
97
97
|
},
|
|
98
98
|
"crow_skills/npm_entry.py": {
|
|
99
99
|
"executable": false,
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"package.json": {
|
|
149
149
|
"executable": false,
|
|
150
150
|
"kind": "file",
|
|
151
|
-
"sha256": "
|
|
151
|
+
"sha256": "9c066a2aa80bb4a853faf9ec8c9d5e0887884e4541539f5e61c62dd369c715b3"
|
|
152
152
|
},
|
|
153
153
|
"skills/crow-accessibility/SKILL.md": {
|
|
154
154
|
"executable": false,
|
|
@@ -968,10 +968,10 @@
|
|
|
968
968
|
"skills/crow-uny-mcp/scripts/crow_mcp.py": {
|
|
969
969
|
"executable": false,
|
|
970
970
|
"kind": "file",
|
|
971
|
-
"sha256": "
|
|
971
|
+
"sha256": "7dd3660097c3afeadb58357939b51da518a4da7cf2d7e684c2618b22f6c4a87b"
|
|
972
972
|
}
|
|
973
973
|
},
|
|
974
|
-
"revision": "
|
|
974
|
+
"revision": "11982e7c39ba1b449a442f8deb6468d5a27d26f4",
|
|
975
975
|
"schema_version": 1,
|
|
976
|
-
"version": "0.38.
|
|
976
|
+
"version": "0.38.2"
|
|
977
977
|
}
|
|
@@ -133,6 +133,72 @@ if sys.platform == "darwin":
|
|
|
133
133
|
pass
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def prune_runtime_records(state_dir, keep_hours=24):
|
|
137
|
+
"""Drop runtime records whose process is gone and that are no longer fresh.
|
|
138
|
+
|
|
139
|
+
Nothing ever removed these. Measured on a real machine: 1402 records, 1148
|
|
140
|
+
of them older than a day. They are tiny (0.4 MB), but the status reader
|
|
141
|
+
takes a bounded window over them, so the dead ones crowded out every live
|
|
142
|
+
server — it reported 2 live pids while 25 servers were running. Pruned by
|
|
143
|
+
whoever creates them; a reader must stay read-only. Metadata caches are
|
|
144
|
+
deliberately NOT touched: discarding a warm-up cache changes behaviour by
|
|
145
|
+
forcing cold starts, which is the owner's call, not hygiene.
|
|
146
|
+
"""
|
|
147
|
+
removed = 0
|
|
148
|
+
try:
|
|
149
|
+
directory = Path(state_dir)
|
|
150
|
+
cutoff = time.time() - keep_hours * 3600
|
|
151
|
+
for path in directory.glob("runtime-*.json"):
|
|
152
|
+
try:
|
|
153
|
+
if path.stat().st_mtime > cutoff:
|
|
154
|
+
continue
|
|
155
|
+
data = json.loads(path.read_text())
|
|
156
|
+
pid = data.get("pid")
|
|
157
|
+
if isinstance(pid, int) and not isinstance(pid, bool) and process_identity(pid) is not None:
|
|
158
|
+
continue # still alive: not ours to remove
|
|
159
|
+
path.unlink()
|
|
160
|
+
removed += 1
|
|
161
|
+
except (OSError, ValueError):
|
|
162
|
+
continue
|
|
163
|
+
except OSError:
|
|
164
|
+
return removed
|
|
165
|
+
return removed
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def serving_names():
|
|
169
|
+
"""pid -> public name of every live `crow_mcp.py serve` on this machine.
|
|
170
|
+
|
|
171
|
+
The runtime registry records state and pids but not the server's NAME, so a
|
|
172
|
+
reader could only report anonymous liveness. The name is on the command
|
|
173
|
+
line; this is the one place allowed to look, because the status module
|
|
174
|
+
deliberately spawns nothing and borrows helpers from here.
|
|
175
|
+
"""
|
|
176
|
+
names = {}
|
|
177
|
+
try:
|
|
178
|
+
out = subprocess.run(["/bin/ps", "-axo", "pid=,command="],
|
|
179
|
+
capture_output=True, text=True, timeout=10)
|
|
180
|
+
except (OSError, subprocess.SubprocessError):
|
|
181
|
+
return names
|
|
182
|
+
if out.returncode != 0:
|
|
183
|
+
return names
|
|
184
|
+
for line in out.stdout.splitlines():
|
|
185
|
+
text = line.strip()
|
|
186
|
+
if "crow_mcp.py" not in text or " serve" not in text:
|
|
187
|
+
continue
|
|
188
|
+
head, _, rest = text.partition(" ")
|
|
189
|
+
try:
|
|
190
|
+
pid = int(head)
|
|
191
|
+
except ValueError:
|
|
192
|
+
continue
|
|
193
|
+
parts = rest.split()
|
|
194
|
+
if "--name" in parts:
|
|
195
|
+
try:
|
|
196
|
+
names[pid] = parts[parts.index("--name") + 1]
|
|
197
|
+
except IndexError:
|
|
198
|
+
continue
|
|
199
|
+
return names
|
|
200
|
+
|
|
201
|
+
|
|
136
202
|
def process_identity(pid):
|
|
137
203
|
"""Kernel start identity, not PID alone or second-granularity ps time."""
|
|
138
204
|
try:
|
|
@@ -173,6 +239,9 @@ class Proxy:
|
|
|
173
239
|
self.input_fd, self.output_fd = input_fd, output_fd
|
|
174
240
|
self.store = PrivateStore(options.state_dir)
|
|
175
241
|
self.registry = "runtime-" + str(os.getpid()) + "-" + uuid.uuid4().hex + ".json"
|
|
242
|
+
# Clean up after the ones that came before: this is the layer that
|
|
243
|
+
# creates the records, so it is the layer that retires them.
|
|
244
|
+
prune_runtime_records(options.state_dir)
|
|
176
245
|
self.sel = selectors.DefaultSelector()
|
|
177
246
|
self.sel.register(input_fd, selectors.EVENT_READ, "host")
|
|
178
247
|
os.set_blocking(output_fd, False)
|