@andespindola/crow-skills 0.16.0 → 0.16.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/README.md +2 -2
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/agents.py +6 -3
- package/crow_skills/broadcast.py +6 -2
- package/crow_skills/guide.py +3 -1
- package/crow_skills/storage.py +30 -2
- package/docs/cli.md +7 -1
- package/package.json +1 -1
- package/release.json +10 -10
package/README.md
CHANGED
|
@@ -11,11 +11,11 @@ Skills com escopo explícito, conhecimento especializado preservado e fluxos com
|
|
|
11
11
|
Uma linha configura todos os CLIs de agente da máquina (Claude Code, Codex, Gemini CLI, Antigravity, Devin, Uniflow e o segmento do tmux):
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
brew install andersonflima/tap/crow-skills && crow-skills setup --yes
|
|
15
|
-
# ou, com npm:
|
|
16
14
|
npm install -g @andespindola/crow-skills && crow-skills setup --yes
|
|
17
15
|
```
|
|
18
16
|
|
|
17
|
+
O pacote é privado no npm: a instalação exige um `~/.npmrc` autenticado no escopo `@andespindola`. O tap do Homebrew foi aposentado na 0.16.1 e está congelado nessa versão — quem instalou por lá troca com `brew uninstall crow-skills` seguido da linha acima.
|
|
18
|
+
|
|
19
19
|
`setup` detecta cada CLI (binário no PATH ou diretório de configuração), instala todas as skills, o guia, as integrações e os hooks do modo crow já habilitados, liga o modo crow e o autoupdate, e termina com uma tabela por CLI e o doctor. `crow-skills setup --dry-run` mostra o plano sem gravar nada; rodar de novo só corrige o que mudou. Para atualizar tudo depois: `crow-skills upgrade --yes`. `crow-skills` sozinho mostra a ajuda curta e o estado atual; `crow-skills help <comando>` detalha cada comando. Detalhes em [setup, upgrade e ajuda](docs/cli.md#configurar-a-máquina-setup).
|
|
20
20
|
|
|
21
21
|
## Uso
|
package/crow_skills/__init__.py
CHANGED
package/crow_skills/agents.py
CHANGED
|
@@ -212,9 +212,12 @@ def refuse_store_as_cli_dir(targets, home):
|
|
|
212
212
|
for target in targets:
|
|
213
213
|
others = [a for a in target.get("agents") or [] if a != "uniflow"]
|
|
214
214
|
if others and physical(target["path"]) == store:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
# "custom" é o rótulo interno de --dest: dizer "o destino de custom" confunde quem não
|
|
216
|
+
# passou agente nenhum. A mensagem nomeia o que o usuário realmente escreveu.
|
|
217
|
+
quem = "--dest" if others == ["custom"] else ",".join(others)
|
|
218
|
+
raise CrowError("protected_path", "O destino de " + quem + " resolve para o store do Uniflow ("
|
|
219
|
+
+ store + "), que vale para todos os CLIs que ele projeta. Use --agents uniflow "
|
|
220
|
+
"para gravar no store.")
|
|
218
221
|
|
|
219
222
|
|
|
220
223
|
def inventory(home):
|
package/crow_skills/broadcast.py
CHANGED
|
@@ -793,8 +793,12 @@ def liveness(snap, age, cfg, now=None, table=None):
|
|
|
793
793
|
if exists is False or is_zombie:
|
|
794
794
|
return None # o processo sumiu, ou é zumbi: a sessão acabou, sem esperar a janela
|
|
795
795
|
recorded = presence.get("cliStart")
|
|
796
|
-
|
|
797
|
-
|
|
796
|
+
# Um heartbeat FRESCO vence a comparação de start: `cliStart` é gravado como
|
|
797
|
+
# (agora_do_hook - etime) e a tabela recalcula (agora_do_leitor - etime), então um salto de
|
|
798
|
+
# relógio (NTP, suspensão do Mac) maior que a tolerância faria os dois divergirem e mataria
|
|
799
|
+
# uma sessão VIVA. Se ela deu sinal agora, está viva, qualquer que seja a aritmética.
|
|
800
|
+
if exists and age > CLI_REUSE_SECONDS and isinstance(start, (int, float)) \
|
|
801
|
+
and isinstance(recorded, (int, float)) and abs(start - recorded) > CLI_REUSE_SECONDS:
|
|
798
802
|
return None # o número do pid foi reciclado por outro processo; esta sessão morreu
|
|
799
803
|
if age <= cfg["stale_minutes"] * 60:
|
|
800
804
|
return "live"
|
package/crow_skills/guide.py
CHANGED
|
@@ -140,7 +140,9 @@ def protect_skill(path, home):
|
|
|
140
140
|
config (config.json, prompts, backups) stay protected. The package entry itself may
|
|
141
141
|
be a link Uniflow made; the plan decides about it, never following it."""
|
|
142
142
|
path = Path(os.path.abspath(path))
|
|
143
|
-
|
|
143
|
+
from .agents import physical
|
|
144
|
+
# Mesmo motivo: o store em dotfiles resolve, e a comparação crua nunca batia.
|
|
145
|
+
if physical(path.parent) == physical(uniflow_store(home)) and NAME.fullmatch(path.name):
|
|
144
146
|
checked(path.parent)
|
|
145
147
|
return path
|
|
146
148
|
path = checked(path)
|
package/crow_skills/storage.py
CHANGED
|
@@ -216,6 +216,22 @@ class Store:
|
|
|
216
216
|
# existe para evitar. Sem resposta única, nenhuma resposta.
|
|
217
217
|
return found[0] if len(found) == 1 else None
|
|
218
218
|
|
|
219
|
+
def retire_alias(self, target):
|
|
220
|
+
"""Retira o estado gravado sob um APELIDO deste destino, depois de gravar o canônico.
|
|
221
|
+
|
|
222
|
+
Sem isto o registro antigo ficava para trás divergente e voltava a mandar se o diretório
|
|
223
|
+
deixasse de ser link: uma skill removida ressuscitava como `missing`, com a revisão errada,
|
|
224
|
+
deixando o doctor insalubre e o update em source_mismatch. Renomeia para `.superseded`
|
|
225
|
+
em vez de apagar, para que nada se perca se o diagnóstico precisar do histórico.
|
|
226
|
+
"""
|
|
227
|
+
alias = self.aliased_state_path(target)
|
|
228
|
+
if alias is None:
|
|
229
|
+
return
|
|
230
|
+
try:
|
|
231
|
+
alias.replace(alias.with_suffix(".superseded.json"))
|
|
232
|
+
except OSError:
|
|
233
|
+
pass # nunca derruba a mutação por causa da aposentadoria do apelido
|
|
234
|
+
|
|
219
235
|
def state(self, target):
|
|
220
236
|
target = resolved(target)
|
|
221
237
|
path = checked(self.state_path(target))
|
|
@@ -354,7 +370,13 @@ class Store:
|
|
|
354
370
|
"crow-skills uninstall " + name + " --dest " + str(target) + " --yes e, se for para o "
|
|
355
371
|
"Uniflow, reinstale com --agents uniflow --global.") from exc
|
|
356
372
|
# Only the uniflow agent replaces Uniflow's import links; --dest keeps refusing links.
|
|
357
|
-
|
|
373
|
+
# physical() e não resolved(): plan() tem um local chamado `resolved` mais abaixo, que
|
|
374
|
+
# torna o nome local em toda a função e faria esta linha estourar UnboundLocalError.
|
|
375
|
+
# O store do Uniflow em dotfiles tem o caminho resolvido, e comparar com o não resolvido
|
|
376
|
+
# deixava o store inteiro não gerenciável (lacuna anterior à 0.16.0).
|
|
377
|
+
from .agents import physical as _physical
|
|
378
|
+
store_target = "uniflow" in target_info["agents"] and \
|
|
379
|
+
str(target) == _physical(uniflow_store(self.home))
|
|
358
380
|
before[key] = state
|
|
359
381
|
after[key] = json.loads(json.dumps(state))
|
|
360
382
|
selected = list(names) if names else list(state["installed"])
|
|
@@ -468,6 +490,7 @@ class Store:
|
|
|
468
490
|
os.replace(work / "new" / item["name"], dest)
|
|
469
491
|
for target, state in data["after"].items():
|
|
470
492
|
atomic_json(self.state_path(target), retargeted(state, target))
|
|
493
|
+
self.retire_alias(target)
|
|
471
494
|
data["phase"] = "committed"
|
|
472
495
|
atomic_json(journal, data)
|
|
473
496
|
except (Exception, KeyboardInterrupt):
|
|
@@ -537,6 +560,7 @@ class Store:
|
|
|
537
560
|
os.replace(backup, dest)
|
|
538
561
|
for target, state in data["before"].items():
|
|
539
562
|
atomic_json(self.state_path(target), retargeted(state, target))
|
|
563
|
+
self.retire_alias(target)
|
|
540
564
|
data["phase"] = "rolled_back"
|
|
541
565
|
atomic_json(self.root / "transactions" / (data["id"] + ".json"), data)
|
|
542
566
|
|
|
@@ -583,7 +607,11 @@ class Store:
|
|
|
583
607
|
continue
|
|
584
608
|
record = state["installed"].get(name)
|
|
585
609
|
link = target / name
|
|
586
|
-
|
|
610
|
+
from .agents import physical as _physical # local `resolved` nesta função sombreia o helper
|
|
611
|
+
# Path(), não a string crua: `physical()` devolve str, e aqui o valor é comparado
|
|
612
|
+
# com um Path (`target == store`, que seria SEMPRE falso, matando em silêncio o
|
|
613
|
+
# ramo de status external_link) e usado como caminho (`store / name`, TypeError).
|
|
614
|
+
store = Path(_physical(uniflow_store(self.home)))
|
|
587
615
|
if link.is_symlink() and target == store:
|
|
588
616
|
resolved = os.path.realpath(link)
|
|
589
617
|
managed = self.managed_target(resolved, name)
|
package/docs/cli.md
CHANGED
|
@@ -228,6 +228,12 @@ O Uniflow guarda as skills num store próprio e cria, ao subir cada CLI, links d
|
|
|
228
228
|
- Cópias nativas duplicadas: se claude, gemini ou devin já têm um pacote crow de mesmo nome, o Uniflow não cria o link para ele e a cópia nativa continua valendo ali, possivelmente em outra revisão (`effect: skipped`). O codex se comporta igual desde a 0.15.0, por instalar no diretório dele (`effect: skipped`); antes ele instalava na pasta compartilhada e o Codex via a mesma skill duas vezes. `install`/`update` com `uniflow` listam essas cópias em `uniflow.native_duplicates` (diretórios que não puderam ser lidos, como um `~/.claude` symlink de dotfiles, vão para `uniflow.uninspected` sem derrubar o comando) e sugerem em `uniflow.suggested_command` o `uninstall` explícito; nada é removido sem esse comando. Remover a cópia nativa da crow-behaviour remove também o guia daquele agente, que deixa de valer fora do Uniflow.
|
|
229
229
|
- Links projetados: em `status`/`doctor`, um link do Uniflow para o store num diretório de agente aparece como `uniflow_link`, e não deixa o doctor `healthy:false`.
|
|
230
230
|
|
|
231
|
+
#### Estado gravado sob um apelido do destino
|
|
232
|
+
|
|
233
|
+
A chave do estado é o caminho FÍSICO. Um estado escrito quando `~/.claude` ainda era diretório comum fica sob outra chave, e `Store.state()` o encontra varrendo os destinos conhecidos (`aliased_state_path`) para não perder a instalação quando você move tudo para dotfiles. Depois de gravar o estado canônico, a mutação APOSENTA o apelido, renomeando-o para `.superseded.json` em vez de apagar. Sem essa aposentadoria o registro antigo ficava divergente e voltava a mandar caso o diretório deixasse de ser link: uma skill removida ressuscitava como `missing`, com a revisão errada, e o `update` passava a recusar com `source_mismatch`. Dois apelidos para o mesmo caminho físico são ambíguos e nenhum é usado.
|
|
234
|
+
|
|
235
|
+
Consequência a conhecer, se você DESFIZER o link depois de ter mexido pelo link: o estado canônico ficou sob o caminho físico de então (o dos dotfiles) e o apelido antigo foi aposentado, então o novo local não tem registro e as skills aparecem como `unmanaged`, com o `doctor` saudável. É a resposta honesta — não existe registro para aquele caminho —, e `install --replace` ou `setup` readotam o que está lá. O comportamento anterior era pior: o registro antigo ressuscitava e reportava uma skill já removida como `missing`, com a revisão errada, deixando o `doctor` insalubre e o `update` em `source_mismatch`.
|
|
236
|
+
|
|
231
237
|
#### Exercitar um hook sem efeito colateral (`--probe`)
|
|
232
238
|
|
|
233
239
|
`crow_hook.py hook --host <host> --event <evento> --probe` calcula exatamente o que o hook entregaria naquele evento — modo crow, aviso de memória e o bloco do crow-broadcast — e **não grava nada**: nenhum evento no barramento, nenhum snapshot de sessão, nenhuma presença, nenhuma escrita de modo. É o que usar para conferir se o hook de um CLI está cabeado e funcionando.
|
|
@@ -549,7 +555,7 @@ O agendador chama o launcher estável (`<prefixo do Homebrew>/bin/crow-skills`,
|
|
|
549
555
|
|
|
550
556
|
Cada execução:
|
|
551
557
|
|
|
552
|
-
1. Atualiza o próprio crow-skills conforme a instalação:
|
|
558
|
+
1. Atualiza o próprio crow-skills conforme a instalação: npm global (`npm install -g @andespindola/crow-skills@latest`), que é a fonte desde a 0.16.1. A detecção de Homebrew (`brew upgrade andersonflima/tap/crow-skills`) continua no código para quem ainda tem uma instalação antiga, mas o tap está congelado na 0.16.1 e não recebe versões novas: um `upgrade` numa instalação brew vai dizer "já na versão mais nova" mesmo havendo versão nova no npm. Checkout Git só com `autoupdate on --git-pull`, e só se estiver limpo em `main`/`master` (`git pull --ff-only`); caso contrário, é pulado com o motivo. Saída de brew/npm/git não é guardada nem impressa, só código de saída e versões.
|
|
553
559
|
2. Se a versão mudou, a versão nova (o launcher) faz o passo seguinte, para usar o snapshot novo como fonte.
|
|
554
560
|
3. Para cada agente com skills gerenciadas (claude, codex/`$CODEX_HOME/skills`, gemini, devin, uniflow), roda o equivalente a `update <skills> --agents <agente> --global --yes`: regenera guia, integrações e hooks; a [identidade do pacote](#origem-e-versões) evita `source_mismatch` depois do upgrade. Skills editadas localmente (`modified`), ausentes ou instaladas de outra origem (`other_origin`, por exemplo de um clone Git) ficam como estão e aparecem em `kept`; nada é sobrescrito. Uma reaplicação dos hooks troca as entradas do crow-skills no lugar, sem mudar a posição das suas. Uma recusa num agente (por exemplo, guia editado) aparece em `error` e não interrompe os outros.
|
|
555
561
|
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"README.md": {
|
|
4
4
|
"executable": false,
|
|
5
5
|
"kind": "file",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "5f0c6838cb5341f1650f19d68b18105c1f0747af1605ac93402f48dc57059cad"
|
|
7
7
|
},
|
|
8
8
|
"bin/npm-crow-skills.cjs": {
|
|
9
9
|
"executable": true,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"crow_skills/__init__.py": {
|
|
19
19
|
"executable": false,
|
|
20
20
|
"kind": "file",
|
|
21
|
-
"sha256": "
|
|
21
|
+
"sha256": "5179ee0b068651b5b2bad6121f285d609552bc99b5efe6cdf40fc6210406cd1f"
|
|
22
22
|
},
|
|
23
23
|
"crow_skills/__main__.py": {
|
|
24
24
|
"executable": false,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"crow_skills/agents.py": {
|
|
29
29
|
"executable": false,
|
|
30
30
|
"kind": "file",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "903750fa57f9aa3cba686bcbd3775881493275b1e5aff86c5e57035d2144c7f1"
|
|
32
32
|
},
|
|
33
33
|
"crow_skills/autoupdate.py": {
|
|
34
34
|
"executable": false,
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"crow_skills/broadcast.py": {
|
|
39
39
|
"executable": false,
|
|
40
40
|
"kind": "file",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "d886806e138953dfe47bd19c8f12b4267f5137e510346081c2e3fb15e4b7a8fb"
|
|
42
42
|
},
|
|
43
43
|
"crow_skills/cli.py": {
|
|
44
44
|
"executable": false,
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"crow_skills/guide.py": {
|
|
59
59
|
"executable": false,
|
|
60
60
|
"kind": "file",
|
|
61
|
-
"sha256": "
|
|
61
|
+
"sha256": "0b5ca9d1629bcdc88e30f42b14b1189f41e9417b46c16771c339fe3ffc09dece"
|
|
62
62
|
},
|
|
63
63
|
"crow_skills/hook.py": {
|
|
64
64
|
"executable": false,
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"crow_skills/storage.py": {
|
|
104
104
|
"executable": false,
|
|
105
105
|
"kind": "file",
|
|
106
|
-
"sha256": "
|
|
106
|
+
"sha256": "2e276f27c25256508670afcd1d6da38811819351dee45e42f4098090a00d8832"
|
|
107
107
|
},
|
|
108
108
|
"crow_skills/trust.py": {
|
|
109
109
|
"executable": false,
|
|
@@ -123,12 +123,12 @@
|
|
|
123
123
|
"docs/cli.md": {
|
|
124
124
|
"executable": false,
|
|
125
125
|
"kind": "file",
|
|
126
|
-
"sha256": "
|
|
126
|
+
"sha256": "0a28be36b02acd9eb7624357c40c91423fbd15c9efaf50e0a06d7577f0879759"
|
|
127
127
|
},
|
|
128
128
|
"package.json": {
|
|
129
129
|
"executable": false,
|
|
130
130
|
"kind": "file",
|
|
131
|
-
"sha256": "
|
|
131
|
+
"sha256": "9b4c64fcd11b8e0e005cdbfb810e0b1d882e707ad27585878e1faf6eb329b2e2"
|
|
132
132
|
},
|
|
133
133
|
"skills/crow-accessibility/SKILL.md": {
|
|
134
134
|
"executable": false,
|
|
@@ -716,7 +716,7 @@
|
|
|
716
716
|
"sha256": "e37132ef8b060548191fbf14a5a18a9dd3acd84ea9a35e802174455b9e374d3d"
|
|
717
717
|
}
|
|
718
718
|
},
|
|
719
|
-
"revision": "
|
|
719
|
+
"revision": "785b744a97831798c047b268bd364d31e1d02903",
|
|
720
720
|
"schema_version": 1,
|
|
721
|
-
"version": "0.16.
|
|
721
|
+
"version": "0.16.2"
|
|
722
722
|
}
|