@andespindola/crow-skills 0.27.0 → 0.28.0
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/README.md +3 -3
- package/catalog.json +15 -0
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/agents.py +35 -70
- package/crow_skills/autoupdate.py +4 -96
- package/crow_skills/broadcast.py +9 -7
- package/crow_skills/cli.py +12 -157
- package/crow_skills/guide.py +24 -64
- package/crow_skills/machine.py +32 -194
- package/crow_skills/projection.py +3 -148
- package/crow_skills/storage.py +54 -86
- package/crow_skills/ui.py +3 -3
- package/docs/cli-security.md +1 -1
- package/docs/cli.md +16 -60
- package/package.json +1 -1
- package/release.json +25 -20
- package/skills/crow-behaviour/SKILL.md +3 -1
- package/skills/crow-behaviour/references/broadcast.md +7 -3
- package/skills/crow-behaviour/references/cross-session-capacity.md +12 -0
- package/skills/crow-ml/SKILL.md +35 -0
package/crow_skills/machine.py
CHANGED
|
@@ -30,7 +30,7 @@ from . import hook
|
|
|
30
30
|
from . import integrations
|
|
31
31
|
from . import resources
|
|
32
32
|
from . import ui
|
|
33
|
-
from .agents import destinations, legacy_shared_dir
|
|
33
|
+
from .agents import destinations, legacy_shared_dir
|
|
34
34
|
from .source import CrowError, Source
|
|
35
35
|
from .storage import Store
|
|
36
36
|
|
|
@@ -47,12 +47,10 @@ CLIS = (
|
|
|
47
47
|
"agent": "gemini", "integrations": ("agy-hooks", "agy-statusline")},
|
|
48
48
|
{"id": "devin", "label": "Devin CLI", "binary": "devin",
|
|
49
49
|
"config": lambda h: Path(destinations("devin", h)[0]["path"]).parent, "agent": "devin", "integrations": ("devin-hooks",)},
|
|
50
|
-
{"id": "uniflow", "label": "Uniflow", "binary": "uniflow", "config": uniflow_config_dir, "agent": "uniflow",
|
|
51
|
-
"integrations": ()},
|
|
52
50
|
{"id": "tmux", "label": "tmux", "binary": "tmux", "flag": "-V", "config": None, "agent": None,
|
|
53
51
|
"integrations": ("tmux-status",)},
|
|
54
52
|
)
|
|
55
|
-
AGENT_ORDER = ("claude", "codex", "gemini", "devin"
|
|
53
|
+
AGENT_ORDER = ("claude", "codex", "gemini", "devin")
|
|
56
54
|
GUIDE_WORDS = {"install": "aplicado", "update": "atualizado", "replace": "atualizado", "unchanged": "já em dia",
|
|
57
55
|
"uninstall": "removido", "remove": "removido"}
|
|
58
56
|
BAD_SKILL = ("modified", "missing", "unsafe", "external_link")
|
|
@@ -81,10 +79,12 @@ def detect(home, probe=True):
|
|
|
81
79
|
rows = []
|
|
82
80
|
for cli in CLIS:
|
|
83
81
|
binary = shutil.which(cli["binary"])
|
|
84
|
-
config =
|
|
82
|
+
config = None
|
|
85
83
|
try:
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
config = cli["config"](home) if cli["config"] else None
|
|
85
|
+
from .agents import physical
|
|
86
|
+
config_found = bool(config and Path(physical(config)).exists())
|
|
87
|
+
except (CrowError, OSError):
|
|
88
88
|
config_found = False
|
|
89
89
|
rows.append({"id": cli["id"], "label": cli["label"], "agent": cli["agent"], "binary": binary,
|
|
90
90
|
"config": str(config) if config else None, "config_found": config_found,
|
|
@@ -149,72 +149,30 @@ def fix_for(code, agent):
|
|
|
149
149
|
return hint
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def store_disabled(home, store):
|
|
156
|
-
"""Names managed by crow-skills in the Uniflow store that Uniflow disabled (SKILL.md.disabled)."""
|
|
157
|
-
try:
|
|
158
|
-
return {r["name"] for r in store.status(destinations("uniflow", home), ()) if r["status"] == "disabled"}
|
|
159
|
-
except (CrowError, OSError):
|
|
160
|
-
return set()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def classify(store, target, names, agent, replace, overwrite, store_names=()):
|
|
164
|
-
"""Split the requested skills into (to install, kept with the reason and fix, projected by Uniflow).
|
|
165
|
-
|
|
166
|
-
A skill Uniflow projects into this CLI (a link to its store) is installed for the CLI: its source of
|
|
167
|
-
truth is the store (--agents uniflow); the link is never followed, replaced or reported as an error."""
|
|
152
|
+
def classify(store, target, names, agent, replace, overwrite):
|
|
153
|
+
"""Select native regular packages, preserving edits and refusing individual links."""
|
|
168
154
|
rows = {r["name"]: r for r in store.status([target], names) if r["name"]}
|
|
169
|
-
install, kept
|
|
170
|
-
# Uniflow already projects into this dir: what goes into its store arrives by the same projection on the
|
|
171
|
-
# CLI's next start; a native copy would shadow it. crow-behaviour stays native when absent (guide, hooks).
|
|
172
|
-
projected = any(r["status"] == "uniflow_link" for r in store.status([target], ()))
|
|
173
|
-
pending = set(store_names) if projected else set()
|
|
174
|
-
store_ok = guide.store_managed(store.home, store) if projected else set()
|
|
175
|
-
disabled = store_disabled(store.home, store) if projected else set()
|
|
176
|
-
broken = guide.store_broken(store.home, store) if projected else set()
|
|
155
|
+
install, kept = [], []
|
|
177
156
|
for name in names:
|
|
178
|
-
|
|
179
|
-
status
|
|
180
|
-
if status == "uniflow_link" and name in store_ok:
|
|
181
|
-
via.append(name)
|
|
182
|
-
elif status == "uniflow_link" and name in disabled:
|
|
183
|
-
kept.append({"name": name, "status": "uniflow_link_disabled", "reason": DISABLED_REASON,
|
|
184
|
-
"fix": "reabilite a skill no Uniflow; o crow-skills mantém a desabilitação"})
|
|
185
|
-
elif status == "uniflow_link" and name in broken:
|
|
186
|
-
kept.append({"name": name, "status": "uniflow_link_modified", "reason": guide.BROKEN_REASON,
|
|
187
|
-
"fix": guide.broken_fix(name)})
|
|
188
|
-
elif status == "uniflow_link":
|
|
189
|
-
kept.append({"name": name, "status": "uniflow_link_unmanaged",
|
|
190
|
-
"reason": "link do Uniflow para um pacote do store que o crow-skills não gerencia; não conta como instalada",
|
|
191
|
-
"fix": "revise o store e, para gerenciá-lo com backup: crow-skills setup --agents uniflow --replace --yes"})
|
|
192
|
-
elif status == "absent" and name in pending and name != "crow-behaviour":
|
|
193
|
-
via.append(name)
|
|
194
|
-
waiting.append(name)
|
|
195
|
-
elif status in ("modified", "missing") and not overwrite:
|
|
157
|
+
status = (rows.get(name) or {"status": "absent"})["status"]
|
|
158
|
+
if status in ("modified", "missing") and not overwrite:
|
|
196
159
|
kept.append({"name": name, "status": status, "reason": "editada localmente" if status == "modified" else "arquivos removidos",
|
|
197
160
|
"fix": "crow-skills setup --agents " + agent + " --overwrite-modified --yes"})
|
|
198
161
|
elif status == "unmanaged" and not replace:
|
|
199
162
|
kept.append({"name": name, "status": status, "reason": "já existe e não é gerenciada pelo crow-skills",
|
|
200
163
|
"fix": "crow-skills setup --agents " + agent + " --replace --yes"})
|
|
201
|
-
elif status == "external_link" and not replace and not store.managed_target(
|
|
202
|
-
os.path.realpath(Path(target["path"]) / name), name):
|
|
203
|
-
kept.append({"name": name, "status": status, "reason": "link do Uniflow para uma cópia não gerenciada",
|
|
204
|
-
"fix": "crow-skills setup --agents uniflow --replace --yes"})
|
|
205
164
|
elif status == "unsafe":
|
|
206
165
|
path = Path(target["path"]) / name
|
|
207
|
-
|
|
208
|
-
kept.append({"name": name, "status": status, "reason": str(path) + " " + what + "; não é lido nem substituído",
|
|
166
|
+
kept.append({"name": name, "status": status, "reason": str(path) + " não é um pacote regular; não é lido nem substituído",
|
|
209
167
|
"fix": "remova ou troque você mesmo " + str(path) + " e rode crow-skills setup --yes de novo"})
|
|
210
168
|
else:
|
|
211
169
|
install.append(name)
|
|
212
|
-
return install, kept
|
|
170
|
+
return install, kept
|
|
213
171
|
|
|
214
172
|
|
|
215
173
|
def refresh_extras(agent, home, dry_run, trust=True, source=None, ref=None, replace=False, overwrite=False):
|
|
216
174
|
"""Guide, integrations and hooks of an agent when no skill is (re)installed there, e.g. every crow
|
|
217
|
-
skill
|
|
175
|
+
skill is already installed. Same commands as guide/integrations install --global."""
|
|
218
176
|
cli = _cli()
|
|
219
177
|
out = {"guides": None, "integrations": None}
|
|
220
178
|
if agent not in guide.MECHANISMS or "crow-behaviour" not in guide.managed_skills(agent, home, Store(home)):
|
|
@@ -235,19 +193,14 @@ def refresh_extras(agent, home, dry_run, trust=True, source=None, ref=None, repl
|
|
|
235
193
|
return out
|
|
236
194
|
|
|
237
195
|
|
|
238
|
-
def configure_agent(args, home, store, agent, names, dry_run
|
|
196
|
+
def configure_agent(args, home, store, agent, names, dry_run):
|
|
239
197
|
cli = _cli()
|
|
240
198
|
target = destinations(agent, home)[0]
|
|
241
|
-
entry = {"agent": agent, "target": target["path"], "changed": [], "unchanged": [], "kept": [],
|
|
199
|
+
entry = {"agent": agent, "target": target["path"], "changed": [], "unchanged": [], "kept": [],
|
|
242
200
|
"error": None, "guides": None, "integrations": None}
|
|
243
201
|
replace, overwrite = bool(getattr(args, "replace", False)), bool(getattr(args, "overwrite_modified", False))
|
|
244
202
|
try:
|
|
245
|
-
install, entry["kept"]
|
|
246
|
-
store_names if agent != "uniflow" else ())
|
|
247
|
-
if agent == "codex":
|
|
248
|
-
install = codex_projection_step(args, home, store, entry, names, install, waiting, store_names, dry_run)
|
|
249
|
-
if waiting:
|
|
250
|
-
entry["via_uniflow_pending"] = waiting
|
|
203
|
+
install, entry["kept"] = classify(store, target, names, agent, replace, overwrite)
|
|
251
204
|
if not install:
|
|
252
205
|
entry.update(refresh_extras(agent, home, dry_run, not getattr(args, "no_trust", False), getattr(args, "source", None),
|
|
253
206
|
getattr(args, "ref", None), replace, overwrite))
|
|
@@ -276,8 +229,6 @@ def configure_agent(args, home, store, agent, names, dry_run, store_names=()):
|
|
|
276
229
|
entry["guides"] = data.get("guides")
|
|
277
230
|
entry["integrations"] = data.get("integrations")
|
|
278
231
|
entry["transaction"] = data.get("transaction")
|
|
279
|
-
if data.get("uniflow"):
|
|
280
|
-
entry["uniflow"] = data["uniflow"]
|
|
281
232
|
if agent == "codex":
|
|
282
233
|
# Depois da instalação no diretório do Codex, nunca antes: a cópia legada só sai quando
|
|
283
234
|
# existe uma instalação no lugar novo. Num dry run o disco não foi tocado, então o que este
|
|
@@ -287,9 +238,6 @@ def configure_agent(args, home, store, agent, names, dry_run, store_names=()):
|
|
|
287
238
|
return entry
|
|
288
239
|
|
|
289
240
|
|
|
290
|
-
DUPLICATE = "removida: duplicava a projeção do Uniflow"
|
|
291
|
-
|
|
292
|
-
|
|
293
241
|
def migrate_legacy(args, home, store, entry, dry_run, planned=()):
|
|
294
242
|
"""legacy_shared_step, but a failure there never costs the agent its result.
|
|
295
243
|
|
|
@@ -304,19 +252,9 @@ def migrate_legacy(args, home, store, entry, dry_run, planned=()):
|
|
|
304
252
|
|
|
305
253
|
|
|
306
254
|
def legacy_shared_step(args, home, store, entry, dry_run, planned=()):
|
|
307
|
-
"""
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
duplicates() only sees names Uniflow projects, so a copy of a NON-projected skill (the
|
|
311
|
-
coordinator itself, typically) would otherwise stay there for ever and keep being listed by
|
|
312
|
-
Codex beside the real install. Only crow-skills-managed, intact copies go, through the normal
|
|
313
|
-
uninstall (backup, transaction, rollback); an edited copy, or one also recorded for another
|
|
314
|
-
agent of that dir, is kept and reported. Nothing that crow-skills did not install is touched.
|
|
315
|
-
|
|
316
|
-
A copy is removed ONLY when that name really works in Codex's own dir — an intact install of
|
|
317
|
-
ours, or a healthy Uniflow link. Otherwise the legacy copy is the only working skill Codex has
|
|
318
|
-
(a broken store entry leaves the link occupying the name in the new dir), and removing it would
|
|
319
|
-
take the skill away instead of migrating it.
|
|
255
|
+
"""Migrate intact managed legacy copies only after native replacements exist.
|
|
256
|
+
|
|
257
|
+
Modified, foreign, unsafe and shared records are retained with evidence.
|
|
320
258
|
"""
|
|
321
259
|
legacy = legacy_shared_dir(home)
|
|
322
260
|
# Mesmo diretório (inclusive via symlink): não há nada para migrar e avisar seria ruído.
|
|
@@ -339,15 +277,10 @@ def legacy_shared_step(args, home, store, entry, dry_run, planned=()):
|
|
|
339
277
|
entry["legacy_shared"] = {"state": "warn", "note": str(exc), "path": str(legacy)}
|
|
340
278
|
return
|
|
341
279
|
# What actually works in Codex's own dir right now: our intact install, or a healthy projection.
|
|
342
|
-
from . import projection
|
|
343
280
|
try:
|
|
344
281
|
own = {r["name"]: r["status"] for r in store.status([destinations("codex", home)[0]], sorted(records)) if r["name"]}
|
|
345
282
|
except (CrowError, OSError):
|
|
346
283
|
own = {}
|
|
347
|
-
try:
|
|
348
|
-
healthy = {k for k, kind in projection.links(home, store).items() if kind == "managed"}
|
|
349
|
-
except (CrowError, OSError):
|
|
350
|
-
healthy = set()
|
|
351
284
|
# One reason per name: a more specific one already recorded for this name (a broken projection,
|
|
352
285
|
# an unsafe target) explains it better than anything this step would add.
|
|
353
286
|
already = {k["name"] for k in entry.get("kept") or []}
|
|
@@ -359,12 +292,12 @@ def legacy_shared_step(args, home, store, entry, dry_run, planned=()):
|
|
|
359
292
|
if name in already:
|
|
360
293
|
deferred.append(name) # já explicado por uma razão mais específica deste mesmo nome
|
|
361
294
|
continue
|
|
362
|
-
if own.get(name) != "installed" and name not in
|
|
295
|
+
if own.get(name) != "installed" and name not in set(planned or ()):
|
|
363
296
|
kept.append({"name": name, "status": "legacy_shared_without_replacement",
|
|
364
297
|
"reason": "a cópia na pasta compartilhada antiga é a única que funciona: no diretório do Codex "
|
|
365
|
-
"este nome não tem instalação íntegra
|
|
298
|
+
"este nome não tem instalação íntegra regular",
|
|
366
299
|
"fix": "conserte a instalação no diretório do Codex (por exemplo crow-skills setup --agents codex "
|
|
367
|
-
"--yes
|
|
300
|
+
"--yes) e rode de novo"})
|
|
368
301
|
continue
|
|
369
302
|
others = set(records.get(name, {}).get("agents") or ["codex"]) - {"codex"}
|
|
370
303
|
if others:
|
|
@@ -399,61 +332,6 @@ def legacy_shared_step(args, home, store, entry, dry_run, planned=()):
|
|
|
399
332
|
"(com backup) agora que o codex instala no diretório dele"}
|
|
400
333
|
|
|
401
334
|
|
|
402
|
-
def codex_projection_step(args, home, store, entry, names, install, waiting, store_names, dry_run):
|
|
403
|
-
"""Codex sees its own dir (where crow-skills installs and Uniflow links its store) and also the legacy
|
|
404
|
-
shared ~/.agents/skills, so a copy left there by a version before 0.15.0 is still listed twice. A name
|
|
405
|
-
Codex sees via a managed projection gets no copy of its own, and an intact managed legacy copy of it is
|
|
406
|
-
removed through uninstall against that legacy dir (backup, transaction, rollback). Returns the names
|
|
407
|
-
still to install."""
|
|
408
|
-
from . import projection
|
|
409
|
-
for name, kind in sorted(projection.links(home, store).items()):
|
|
410
|
-
if kind == "modified" and name in names:
|
|
411
|
-
entry["kept"].append({"name": name, "status": "uniflow_link_modified",
|
|
412
|
-
"reason": guide.BROKEN_REASON + "; o link do Uniflow ocupa este nome no diretório do "
|
|
413
|
-
"Codex, então conserte a entrada do store",
|
|
414
|
-
"fix": guide.broken_fix(name)})
|
|
415
|
-
projected = projection.projected("codex", home, store)
|
|
416
|
-
if not projected:
|
|
417
|
-
return install
|
|
418
|
-
selected = set(names)
|
|
419
|
-
# Names on their way into the store reach Codex by the same projection on its next start.
|
|
420
|
-
pending = (set(store_names) - projected - {"crow-behaviour"}) & set(install)
|
|
421
|
-
pending = {n for n in pending if n not in entry["unchanged"]}
|
|
422
|
-
native = {r["name"]: r["status"] for r in store.status([destinations("codex", home)[0]], sorted(pending)) if r["name"]}
|
|
423
|
-
pending = {n for n in pending if native.get(n, "absent") == "absent"}
|
|
424
|
-
entry["via_uniflow"] = sorted(set(entry["via_uniflow"]) | (projected & selected) | pending)
|
|
425
|
-
waiting.extend(sorted(pending))
|
|
426
|
-
dupes = projection.duplicates(home, store)
|
|
427
|
-
overwrite = bool(getattr(args, "overwrite_modified", False))
|
|
428
|
-
remove = [n for n in dupes["remove"] + (dupes["modified"] if overwrite else []) if n in selected]
|
|
429
|
-
entry["kept"] = [k for k in entry["kept"] if k["name"] not in projected]
|
|
430
|
-
for name in dupes["modified"]:
|
|
431
|
-
if name in selected and not overwrite:
|
|
432
|
-
entry["kept"].append({"name": name, "status": "duplicate_modified",
|
|
433
|
-
"reason": "cópia editada na pasta compartilhada antiga, que duplica a projeção do Uniflow no Codex",
|
|
434
|
-
"fix": "revise e, para remover com backup: crow-skills setup --agents codex --overwrite-modified --yes"})
|
|
435
|
-
for name in dupes["shared"]:
|
|
436
|
-
if name in selected:
|
|
437
|
-
entry["kept"].append({"name": name, "status": "duplicate_shared",
|
|
438
|
-
"reason": "cópia na pasta compartilhada antiga também registrada para outro agente que lê "
|
|
439
|
-
"aquele diretório; mantida, e o Codex a vê duas vezes",
|
|
440
|
-
"fix": "se nenhum outro host precisa dela: crow-skills uninstall " + name +
|
|
441
|
-
" --dest " + str(legacy_shared_dir(home)) + " --yes"})
|
|
442
|
-
if remove:
|
|
443
|
-
cli = _cli()
|
|
444
|
-
# The duplicates live in the legacy shared folder, never in Codex's own dir: --dest targets it
|
|
445
|
-
# explicitly, since no agent name points there any more.
|
|
446
|
-
argv = ["uninstall", *remove, "--dest", str(legacy_shared_dir(home)), "--json", "--home", str(home),
|
|
447
|
-
"--dry-run" if dry_run else "--yes"]
|
|
448
|
-
if overwrite:
|
|
449
|
-
argv.append("--overwrite-modified")
|
|
450
|
-
argv.append("--no-integrations") # hooks/trust follow in the install/refresh step, with the run's --no-trust
|
|
451
|
-
data = cli.run(cli.parser().parse_args(argv))
|
|
452
|
-
entry["removed"] = [{"name": c["name"], "reason": DUPLICATE} for c in data["changes"] if c["action"] != "unchanged"]
|
|
453
|
-
entry["removal_transaction"] = data.get("transaction")
|
|
454
|
-
return [n for n in install if n not in projected and n not in pending]
|
|
455
|
-
|
|
456
|
-
|
|
457
335
|
def machine_steps(args, home, store, dry_run):
|
|
458
336
|
steps = {}
|
|
459
337
|
# Crow mode: written as on only when the user never chose (no mode file).
|
|
@@ -589,10 +467,6 @@ def summarize(detected, entries, store, home, probe, dry_run, trust=True):
|
|
|
589
467
|
parts.append(str(changed) + " " + verb)
|
|
590
468
|
if unchanged:
|
|
591
469
|
parts.append(str(unchanged) + " já em dia")
|
|
592
|
-
if entry.get("via_uniflow"):
|
|
593
|
-
parts.append(str(len(entry["via_uniflow"])) + " via uniflow")
|
|
594
|
-
if entry.get("removed"):
|
|
595
|
-
parts.append(str(len(entry["removed"])) + (" a remover" if dry_run else " removidas") + " (duplicavam o Uniflow)")
|
|
596
470
|
if kept:
|
|
597
471
|
parts.append(str(len(kept)) + " mantidas")
|
|
598
472
|
legacy = entry.get("legacy_shared") or {}
|
|
@@ -609,9 +483,7 @@ def summarize(detected, entries, store, home, probe, dry_run, trust=True):
|
|
|
609
483
|
guides = entry.get("guides") or {}
|
|
610
484
|
guide_change = next((c for c in guides.get("changes", []) if c.get("agent") == cli["agent"]), None)
|
|
611
485
|
guide_skip = next((s for s in guides.get("skipped", []) if s.get("agent") == cli["agent"]), None)
|
|
612
|
-
if
|
|
613
|
-
item["guide"] = "não se aplica"
|
|
614
|
-
elif guide_change:
|
|
486
|
+
if guide_change:
|
|
615
487
|
item["guide"] = GUIDE_WORDS.get(guide_change.get("action"), guide_change.get("action", "?"))
|
|
616
488
|
elif guide_skip:
|
|
617
489
|
item["guide"] = "não aplicado"
|
|
@@ -637,19 +509,6 @@ def summarize(detected, entries, store, home, probe, dry_run, trust=True):
|
|
|
637
509
|
item["notes"].append("hooks: " + fix)
|
|
638
510
|
for k in kept:
|
|
639
511
|
item["notes"].append(k["name"] + " mantida (" + k["reason"] + "): " + k["fix"])
|
|
640
|
-
if entry.get("via_uniflow_pending"):
|
|
641
|
-
item["notes"].append(", ".join(entry["via_uniflow_pending"]) + ": vai para o store do Uniflow e aparece neste CLI no "
|
|
642
|
-
"próximo início pelo Uniflow (sem cópia nativa, para não sombrear o store)")
|
|
643
|
-
for dup in (entry.get("uniflow") or {}).get("native_duplicates") or []:
|
|
644
|
-
names_ = ", ".join(sk["name"] for sk in dup["skills"])
|
|
645
|
-
effect = ("o Codex vê as duas cópias" if dup.get("effect") == "both" else
|
|
646
|
-
"o Uniflow não liga esse nome e a cópia nativa vale ali")
|
|
647
|
-
item["notes"].append("cópia física em " + ",".join(dup["agents"]) + " (" + dup["target"] + ") de " + names_ +
|
|
648
|
-
", que o store também tem: " + effect + " (links do Uniflow não contam)")
|
|
649
|
-
for command in (entry.get("uniflow") or {}).get("suggested_commands") or []:
|
|
650
|
-
item["notes"].append("para usar só o store: " + command + " --yes" +
|
|
651
|
-
(" (sem a cópia nativa da crow-behaviour, o guia daquele agente sai até o Uniflow ligar o store)"
|
|
652
|
-
if " crow-behaviour " in command else ""))
|
|
653
512
|
problems = [] if dry_run else agent_health(store, home, cli["agent"])
|
|
654
513
|
if entry["error"]:
|
|
655
514
|
item["state"] = "fail"
|
|
@@ -674,18 +533,7 @@ def setup(args, home, store, interactive):
|
|
|
674
533
|
source, names = catalog_names(args)
|
|
675
534
|
|
|
676
535
|
def build(dry):
|
|
677
|
-
|
|
678
|
-
# (installed, updated or already there); kept or failed names are installed natively.
|
|
679
|
-
entries = {}
|
|
680
|
-
store_names = ()
|
|
681
|
-
if "uniflow" in agents:
|
|
682
|
-
entries["uniflow"] = configure_agent(args, home, store, "uniflow", names, dry)
|
|
683
|
-
if not entries["uniflow"]["error"]:
|
|
684
|
-
store_names = tuple(entries["uniflow"]["changed"]) + tuple(entries["uniflow"]["unchanged"])
|
|
685
|
-
for agent in agents:
|
|
686
|
-
if agent != "uniflow":
|
|
687
|
-
entries[agent] = configure_agent(args, home, store, agent, names, dry, store_names)
|
|
688
|
-
entries = [entries[a] for a in agents]
|
|
536
|
+
entries = [configure_agent(args, home, store, agent, names, dry) for agent in agents]
|
|
689
537
|
return entries, machine_steps(args, home, store, dry)
|
|
690
538
|
|
|
691
539
|
entries, steps = build(True)
|
|
@@ -748,8 +596,7 @@ def doctor_reasons(doctor, trust=True):
|
|
|
748
596
|
|
|
749
597
|
# --- upgrade -------------------------------------------------------------------------------
|
|
750
598
|
|
|
751
|
-
AGENT_LABELS = {"claude": "Claude Code", "codex": "Codex CLI", "gemini": "Gemini CLI / Antigravity", "devin": "Devin CLI"
|
|
752
|
-
"uniflow": "Uniflow"}
|
|
599
|
+
AGENT_LABELS = {"claude": "Claude Code", "codex": "Codex CLI", "gemini": "Gemini CLI / Antigravity", "devin": "Devin CLI"}
|
|
753
600
|
|
|
754
601
|
|
|
755
602
|
def upgrade_summary(skills, dry_run):
|
|
@@ -757,22 +604,13 @@ def upgrade_summary(skills, dry_run):
|
|
|
757
604
|
for s in skills:
|
|
758
605
|
agent = s["agent"]
|
|
759
606
|
item = {"agent": agent, "label": AGENT_LABELS.get(agent, agent), "changed": s.get("updated") or [],
|
|
760
|
-
"unchanged": s.get("unchanged", 0), "kept": s.get("kept") or [],
|
|
607
|
+
"unchanged": s.get("unchanged", 0), "kept": s.get("kept") or [],
|
|
761
608
|
"removed": [r["name"] for r in s.get("removed") or []],
|
|
762
609
|
"needs_user": []}
|
|
763
610
|
for k in item["kept"]:
|
|
764
611
|
if k["status"] in ("modified", "missing"):
|
|
765
612
|
item["needs_user"].append(k["name"] + " editada localmente; para substituir com backup: crow-skills update " +
|
|
766
613
|
k["name"] + " --agents " + agent + " --global --overwrite-modified --yes")
|
|
767
|
-
elif k["status"] == "duplicate_modified":
|
|
768
|
-
item["needs_user"].append(k["name"] + " (cópia nativa editada) duplica a projeção do Uniflow no Codex; para remover "
|
|
769
|
-
"com backup: crow-skills setup --agents codex --overwrite-modified --yes")
|
|
770
|
-
elif k["status"] == "uniflow_link_modified":
|
|
771
|
-
item["needs_user"].append(k["name"] + ": a entrada do store do Uniflow foi editada ou perdeu arquivos; para "
|
|
772
|
-
"restaurá-la com backup: " + k.get("fix", "crow-skills update " + k["name"] +
|
|
773
|
-
" --agents uniflow --global --overwrite-modified --yes").split(": ", 1)[-1])
|
|
774
|
-
elif k["status"] == "duplicate_shared":
|
|
775
|
-
item["needs_user"].append(k["name"] + " duplica a projeção do Uniflow no Codex, mas também é de shared; mantida")
|
|
776
614
|
elif k["status"] == "other_origin":
|
|
777
615
|
item["needs_user"].append(k["name"] + " instalada de outra origem; para trocar: crow-skills install " +
|
|
778
616
|
k["name"] + " --agents " + agent + " --global --yes")
|
|
@@ -881,7 +719,7 @@ def render_setup(data, stream=None):
|
|
|
881
719
|
lines.append("")
|
|
882
720
|
if data.get("nothing_detected"):
|
|
883
721
|
lines.append(ui.mark("warn", stream) + " Nenhum CLI de agente encontrado. Instale um (claude, codex, gemini, agy, devin, "
|
|
884
|
-
"
|
|
722
|
+
"devin) e rode crow-skills setup de novo.")
|
|
885
723
|
elif data.get("dry_run"):
|
|
886
724
|
lines.append("Para aplicar: crow-skills setup --yes")
|
|
887
725
|
elif data.get("healthy"):
|
|
@@ -914,7 +752,7 @@ def render_upgrade(data, stream=None):
|
|
|
914
752
|
for item in data.get("summary", []):
|
|
915
753
|
changed = ", ".join(item["changed"]) if len(item["changed"]) <= 3 else str(len(item["changed"])) + " skills"
|
|
916
754
|
removed = ((" · " if changed else "") + str(len(item["removed"])) + (" a remover" if dry else " removidas") +
|
|
917
|
-
" (
|
|
755
|
+
" (cópias legadas)") if item.get("removed") else ""
|
|
918
756
|
fresh = item.get("installed_new") or []
|
|
919
757
|
# Dito na própria linha do CLI: uma skill nova do catálogo não é "atualizada", é instalada
|
|
920
758
|
# agora — e foi exatamente isso que faltou quando a 0.17.0 publicou a crow-be-human.
|
|
@@ -923,10 +761,10 @@ def render_upgrade(data, stream=None):
|
|
|
923
761
|
rows.append([ui.mark(STATE_MARK[item["state"]], stream), item["label"],
|
|
924
762
|
((("a atualizar: " if dry else "atualizadas: ") + changed) if changed else ("" if removed or new_text else "sem mudanças"))
|
|
925
763
|
+ removed + new_text,
|
|
926
|
-
str(item["unchanged"]), str(
|
|
764
|
+
str(item["unchanged"]), str(len(item["kept"]))])
|
|
927
765
|
if rows:
|
|
928
766
|
lines.append("")
|
|
929
|
-
lines += ui.table(rows, ["", "CLI", "mudanças", "em dia", "
|
|
767
|
+
lines += ui.table(rows, ["", "CLI", "mudanças", "em dia", "mantidas"], stream)
|
|
930
768
|
elif not data.get("handoff"):
|
|
931
769
|
lines += ["", " Nenhum agente com skills gerenciadas; comece com: crow-skills setup --yes"]
|
|
932
770
|
needs = [(i["label"], n) for i in data.get("summary", []) for n in i["needs_user"]]
|
|
@@ -1,127 +1,15 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
Since 0.15.0 crow-skills installs for codex in that same directory, like it does for every other CLI, so a
|
|
4
|
-
managed copy and a projected link can no longer carry the same name in two places. Codex still scans
|
|
5
|
-
~/.agents/skills as well, so a copy left there by an older version is still listed twice: those legacy copies
|
|
6
|
-
are what duplicates() reports, and removing them is the migration away from the shared folder.
|
|
7
|
-
|
|
8
|
-
A link counts as the codex install of a skill ("via uniflow") only when it resolves to <store>/<name> and
|
|
9
|
-
that store entry is crow-skills-managed (guide.store_managed); a store entry Uniflow disabled is reported as
|
|
10
|
-
disabled; a link to anything else is ignored. A link ~/.codex/skills/crow-x -> ~/.agents/skills/crow-x is an
|
|
11
|
-
alias Codex deduplicates by canonical path: never altered nor removed. Note that the LINK being untouched
|
|
12
|
-
does not make the command succeed — that name now lives in the codex destination, and a symlinked entry
|
|
13
|
-
there cannot be managed, so install/setup report it (status "unsafe") and exit 1 until the user resolves it.
|
|
14
|
-
"""
|
|
1
|
+
"""Generic migration of pre-0.15 native Codex copies from ~/.agents/skills."""
|
|
15
2
|
import os
|
|
16
|
-
from
|
|
17
|
-
|
|
18
|
-
from .agents import codex_home, codex_skills_dir, destinations, legacy_shared_dir, real_home, uniflow_store
|
|
3
|
+
from .agents import destinations, legacy_shared_dir
|
|
19
4
|
from .source import CrowError
|
|
20
5
|
|
|
21
|
-
|
|
22
|
-
def links(home, store):
|
|
23
|
-
"""{name: "managed" | "disabled" | "modified" | "unmanaged" | "alias" | "other"} for crow-* symlinks in Codex's own
|
|
24
|
-
skills dir: to an intact managed / Uniflow-disabled / edited-or-incomplete managed / hand-written store entry, to our
|
|
25
|
-
legacy shared copy, or elsewhere. Only "managed" counts as the codex install and allows removing a legacy
|
|
26
|
-
duplicate. "alias" is a link into the legacy ~/.agents/skills: Codex deduplicates it by canonical path, and
|
|
27
|
-
crow-skills never alters it — but it occupies the name in the destination, so that name is reported
|
|
28
|
-
unmanageable rather than installed."""
|
|
29
|
-
directory = codex_skills_dir(home)
|
|
30
|
-
try:
|
|
31
|
-
if directory.is_symlink() or not directory.is_dir():
|
|
32
|
-
return {}
|
|
33
|
-
entries = [p for p in directory.iterdir() if p.name.startswith("crow-") and p.is_symlink()]
|
|
34
|
-
except OSError:
|
|
35
|
-
return {}
|
|
36
|
-
if not entries:
|
|
37
|
-
return {}
|
|
38
|
-
store_dir = uniflow_store(home)
|
|
39
|
-
# The alias case is a link into the LEGACY shared folder, never into Codex's own dir (where the link
|
|
40
|
-
# itself lives): comparing against the destination would now test a link against itself.
|
|
41
|
-
legacy = legacy_shared_dir(home)
|
|
42
|
-
from .guide import store_statuses
|
|
43
|
-
statuses = store_statuses(home, store) # only an intact ("installed") store entry is a working codex install
|
|
44
|
-
result = {}
|
|
45
|
-
for link in entries:
|
|
46
|
-
name = link.name
|
|
47
|
-
resolved = os.path.realpath(link)
|
|
48
|
-
if resolved == os.path.realpath(legacy / name):
|
|
49
|
-
result[name] = "alias"
|
|
50
|
-
elif (resolved == os.path.realpath(store_dir / name) and (store_dir / name).is_dir()
|
|
51
|
-
and not (store_dir / name).is_symlink()):
|
|
52
|
-
state = statuses.get(name)
|
|
53
|
-
result[name] = {"installed": "managed", "disabled": "disabled", "modified": "modified",
|
|
54
|
-
"missing": "modified"}.get(state, "unmanaged")
|
|
55
|
-
else:
|
|
56
|
-
result[name] = "other"
|
|
57
|
-
return result
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def status_rows(home, store, rows):
|
|
61
|
-
"""Rows for `status`/`doctor`: Codex's view through Uniflow's links, classified.
|
|
62
|
-
|
|
63
|
-
Codex's own dir IS the codex destination since 0.15.0, so `store.status()` already lists these
|
|
64
|
-
links (as a bare `uniflow_link`). These rows ENRICH the existing one for each name — managed,
|
|
65
|
-
disabled, edited or unmanaged, each with its reason — and are only appended when the base scan
|
|
66
|
-
did not report the name. Appending unconditionally would count every projected skill twice.
|
|
67
|
-
"""
|
|
68
|
-
directory = str(codex_skills_dir(home))
|
|
69
|
-
extra = []
|
|
70
|
-
for name, kind in sorted(links(home, store).items()):
|
|
71
|
-
if kind == "managed":
|
|
72
|
-
extra.append({"name": name, "target": directory, "agents": ["codex"], "status": "uniflow_link", "revision": None,
|
|
73
|
-
"activation_verified": False, "projection": "codex_home",
|
|
74
|
-
"note": "Link do Uniflow em " + directory + " para o store; é a instalação do codex (via uniflow)."})
|
|
75
|
-
elif kind == "disabled":
|
|
76
|
-
extra.append({"name": name, "target": directory, "agents": ["codex"], "status": "disabled", "revision": None,
|
|
77
|
-
"activation_verified": False, "projection": "uniflow_link_disabled",
|
|
78
|
-
"note": "Desabilitada no Uniflow (SKILL.md.disabled no store); reabilite no Uniflow para usar."})
|
|
79
|
-
elif kind == "modified":
|
|
80
|
-
from .guide import BROKEN_REASON, broken_fix
|
|
81
|
-
extra.append({"name": name, "target": directory, "agents": ["codex"], "status": "modified", "revision": None,
|
|
82
|
-
"activation_verified": False, "projection": "uniflow_link_modified",
|
|
83
|
-
"note": BROKEN_REASON[0].upper() + BROKEN_REASON[1:] + ". Para corrigir: " + broken_fix(name)})
|
|
84
|
-
elif kind == "unmanaged":
|
|
85
|
-
extra.append({"name": name, "target": directory, "agents": ["codex"], "status": "unmanaged", "revision": None,
|
|
86
|
-
"activation_verified": False, "projection": "uniflow_link_unmanaged",
|
|
87
|
-
"note": "Link do Uniflow para um pacote do store que o crow-skills não gerencia; não conta como instalada. "
|
|
88
|
-
"Revise o store e, para gerenciá-lo com backup: crow-skills setup --agents uniflow --replace --yes"})
|
|
89
|
-
return merge_rows(rows, extra, directory)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def merge_rows(rows, extra, directory):
|
|
93
|
-
"""Each enriched row replaces the base row of the same name in the same directory; the rest is
|
|
94
|
-
appended. Returns the merged list (the caller keeps its own list object)."""
|
|
95
|
-
merged, used = [], set()
|
|
96
|
-
for row in rows:
|
|
97
|
-
richer = next((e for e in extra if e["name"] == row.get("name") and row.get("target") == directory), None)
|
|
98
|
-
if richer is None:
|
|
99
|
-
merged.append(row)
|
|
100
|
-
continue
|
|
101
|
-
used.add(richer["name"])
|
|
102
|
-
merged.append(dict(row, **{k: v for k, v in richer.items() if k != "revision" or row.get("revision") is None}))
|
|
103
|
-
merged += [e for e in extra if e["name"] not in used]
|
|
104
|
-
return merged
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def projected(agent, home, store):
|
|
108
|
-
"""Names Codex already sees through a managed Uniflow projection (empty for other agents)."""
|
|
109
|
-
if agent != "codex":
|
|
110
|
-
return set()
|
|
111
|
-
return {n for n, kind in links(home, store).items() if kind == "managed"}
|
|
112
|
-
|
|
113
|
-
|
|
114
6
|
def native_target(agent, target, home):
|
|
115
7
|
"""target is the user-scope codex install dir (not --project/--dest)."""
|
|
116
8
|
return agent == "codex" and os.path.abspath(target["path"]) == os.path.abspath(destinations("codex", home)[0]["path"])
|
|
117
9
|
|
|
118
10
|
|
|
119
11
|
def legacy_leftovers(home, store):
|
|
120
|
-
"""Names
|
|
121
|
-
|
|
122
|
-
duplicates() answers "what duplicates a projection", so on a machine without Uniflow it is
|
|
123
|
-
always empty — and an install made by 0.14.0 (which put codex's copies in that folder) would be
|
|
124
|
-
invisible to status/doctor while Codex kept loading it. This is the signal for that."""
|
|
12
|
+
"""Names still managed in the former shared native destination."""
|
|
125
13
|
legacy = legacy_shared_dir(home)
|
|
126
14
|
if not legacy.is_dir() or legacy.is_symlink():
|
|
127
15
|
return []
|
|
@@ -135,36 +23,3 @@ def legacy_leftovers(home, store):
|
|
|
135
23
|
return []
|
|
136
24
|
return sorted(n for n, r in rows.items() if r["status"] in ("installed", "modified", "missing"))
|
|
137
25
|
|
|
138
|
-
|
|
139
|
-
def duplicates(home, store):
|
|
140
|
-
"""Copies left in the legacy shared folder (~/.agents/skills) that Codex lists a second time.
|
|
141
|
-
|
|
142
|
-
Codex scans its own dir and ~/.agents/skills; until 0.15.0 crow-skills installed for codex in the
|
|
143
|
-
latter. A copy still there duplicates what Uniflow projects into Codex's own dir, so it is the thing
|
|
144
|
-
to migrate away. Returns {"remove": [...], "modified": [...], "shared": [...]}: "remove" are
|
|
145
|
-
crow-skills-managed and intact; "modified" were edited locally (kept unless --overwrite-modified);
|
|
146
|
-
"shared" are also recorded for another agent of that dir, which may rely on them."""
|
|
147
|
-
names = projected("codex", home, store)
|
|
148
|
-
result = {"remove": [], "modified": [], "shared": []}
|
|
149
|
-
if not names:
|
|
150
|
-
return result
|
|
151
|
-
legacy = legacy_shared_dir(home)
|
|
152
|
-
if not legacy.is_dir() or legacy.is_symlink():
|
|
153
|
-
return result
|
|
154
|
-
target = {"path": str(legacy), "agents": ["codex"]}
|
|
155
|
-
try:
|
|
156
|
-
rows = {r["name"]: r for r in store.status([target], sorted(names)) if r["name"]}
|
|
157
|
-
records = store.state(target["path"])["installed"]
|
|
158
|
-
except (CrowError, OSError):
|
|
159
|
-
return result
|
|
160
|
-
for name in sorted(names):
|
|
161
|
-
row = rows.get(name)
|
|
162
|
-
if not row or row["status"] not in ("installed", "modified", "missing"):
|
|
163
|
-
continue # absent, unmanaged or unreadable: nothing of ours to remove
|
|
164
|
-
if set(records.get(name, {}).get("agents") or ["codex"]) - {"codex"}:
|
|
165
|
-
result["shared"].append(name)
|
|
166
|
-
elif row["status"] == "installed":
|
|
167
|
-
result["remove"].append(name)
|
|
168
|
-
else:
|
|
169
|
-
result["modified"].append(name)
|
|
170
|
-
return result
|