@passioncode-ai/fabric-agent-adapter 0.40.0 → 0.42.1
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/CHANGELOG.md +160 -0
- package/README.md +9 -3
- package/SECURITY.md +27 -6
- package/bin/fabric-agent-adapter.js +71 -6
- package/package.json +2 -2
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +28 -30
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +47 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +230 -29
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +68 -24
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +53 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +145 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +196 -188
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +5 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +48 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +34 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +157 -22
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +6 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +162 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +207 -227
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +0 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +52 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +101 -45
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +12 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +5 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +13 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +118 -71
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +272 -74
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +36 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +270 -35
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +36 -16
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +11 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +44 -22
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +54 -6
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +72 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +28 -7
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +277 -49
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +10 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py
CHANGED
|
@@ -57,7 +57,7 @@ SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
|
|
|
57
57
|
LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
VERSION = "0.
|
|
60
|
+
VERSION = "0.42.1"
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
|
|
@@ -401,24 +401,72 @@ def _process(pid: int) -> Tuple[Optional[str], int]:
|
|
|
401
401
|
return None, 0
|
|
402
402
|
|
|
403
403
|
|
|
404
|
-
def launched_by_ancestor(env: Optional[Dict[str, str]] = None, depth: int = 4
|
|
404
|
+
def launched_by_ancestor(env: Optional[Dict[str, str]] = None, depth: int = 4,
|
|
405
|
+
process: Optional[Callable[[int], Tuple[Optional[str], int]]] = None,
|
|
406
|
+
platform: str = sys.platform) -> Optional[bool]:
|
|
405
407
|
"""A Claude Code Fabric launched runs with `--mcp-config <Fabric>/sessions/<id>/mcp.json` (sessionBundle.ts,
|
|
406
|
-
adapter mcp-config-flag) and no environment marker yet: look for that in this hook's few ancestors.
|
|
407
|
-
|
|
408
|
+
adapter mcp-config-flag) and no environment marker yet: look for that in this hook's few ancestors. True or False
|
|
409
|
+
once the walk is done; None when a process could not be read (a `ps` that failed or ran out of time), which is
|
|
410
|
+
"unknown", never "not launched" (FAA-68). Windows has no walk (`_process` reads no process there): False, so
|
|
411
|
+
only the environment signals count. Only the path is matched; nothing read here is kept or logged."""
|
|
412
|
+
if process is None and platform == "win32":
|
|
413
|
+
return False
|
|
408
414
|
marker = os.path.normpath(str(fabric_root(env=env) / "sessions")) + os.sep
|
|
415
|
+
process = process or _process
|
|
409
416
|
pid = os.getppid()
|
|
410
417
|
for _ in range(depth):
|
|
411
418
|
if pid <= 1:
|
|
412
419
|
return False
|
|
413
|
-
command, parent =
|
|
420
|
+
command, parent = process(pid)
|
|
414
421
|
if command is None:
|
|
415
|
-
return
|
|
422
|
+
return None
|
|
416
423
|
if "--mcp-config" in command and marker in command:
|
|
417
424
|
return True
|
|
418
425
|
pid = parent
|
|
419
426
|
return False
|
|
420
427
|
|
|
421
428
|
|
|
429
|
+
#: A session rejoins at most once in this span (FAA-53): every rejoin spends Fabric's per-agent request budget, which
|
|
430
|
+
#: every session of the machine shares. The Stop worker and watch-mail both honour and renew the mark (FAA-68).
|
|
431
|
+
REJOIN_MIN_INTERVAL_S = 60.0
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
#: The file in a session's folder that says Fabric launched it: the ancestor walk ran once and found the launcher.
|
|
435
|
+
LAUNCHED_MARK = "launched"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
#: The file in a session's folder that says Fabric held no session for its id: fabric-mcp --rejoin-only exited 3
|
|
439
|
+
#: (Fabric 0.3.9, TR-11). For NO_SESSION_HOLD_S the hooks spool nothing for that id; after it, one rejoin tries again,
|
|
440
|
+
#: so a Fabric that restarted and enrolled the id again is found (board thread 02bf19c5, ask 4).
|
|
441
|
+
NO_SESSION_MARK = "no-session"
|
|
442
|
+
NO_SESSION_HOLD_S = 15 * 60.0
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def no_session_held(directory: Path, now: Optional[float] = None) -> bool:
|
|
446
|
+
"""Whether a rejoin found no Fabric session for this id within NO_SESSION_HOLD_S."""
|
|
447
|
+
try:
|
|
448
|
+
mark = (directory / NO_SESSION_MARK).stat().st_mtime
|
|
449
|
+
except OSError:
|
|
450
|
+
return False
|
|
451
|
+
age = (time.time() if now is None else now) - mark
|
|
452
|
+
return 0 <= age < NO_SESSION_HOLD_S
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def rejoined_recently(directory: Path, now: Optional[float] = None) -> bool:
|
|
456
|
+
"""Whether this session's last rejoin succeeded within REJOIN_MIN_INTERVAL_S (the worker marks it)."""
|
|
457
|
+
return rejoin_wait(directory, now) > 0
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def rejoin_wait(directory: Path, now: Optional[float] = None) -> float:
|
|
461
|
+
"""Seconds until this session may rejoin again; 0 when it may now."""
|
|
462
|
+
try:
|
|
463
|
+
mark = (directory / "rejoined").stat().st_mtime
|
|
464
|
+
except OSError:
|
|
465
|
+
return 0.0
|
|
466
|
+
age = (time.time() if now is None else now) - mark
|
|
467
|
+
return REJOIN_MIN_INTERVAL_S - age if 0 <= age < REJOIN_MIN_INTERVAL_S else 0.0
|
|
468
|
+
|
|
469
|
+
|
|
422
470
|
def log(line: str, env: Optional[Dict[str, str]] = None) -> None:
|
|
423
471
|
"""One structured line, 0600, rotated at 1 MB. Never raises."""
|
|
424
472
|
try:
|
package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import sys
|
|
9
10
|
import time
|
|
10
11
|
from pathlib import Path
|
|
@@ -101,6 +102,52 @@ def _kimi_block(command: str) -> str:
|
|
|
101
102
|
"[[hooks]]\nevent = \"SessionStart\"\ncommand = %s\ntimeout = 5\n" % json.dumps(command))
|
|
102
103
|
|
|
103
104
|
|
|
105
|
+
_KIMI_COMMENT = _kimi_block("").split("[[hooks]]", 1)[0].lstrip("\n").splitlines(True) # our two comment lines
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _kimi_spans(text: str, host: str) -> List[Tuple[int, int]]:
|
|
109
|
+
"""Where each `[[hooks]]` table whose command is ours sits in `text`, as line ranges [header, end). A table ends at
|
|
110
|
+
the next header; trailing blank and comment lines belong to what follows. Each table is read on its own with
|
|
111
|
+
tomllib, so a hand-touched table (another order, a comment, another timeout) is still found by what it runs."""
|
|
112
|
+
lines = text.splitlines(True)
|
|
113
|
+
headers = [i for i, line in enumerate(lines) if line.startswith("[")]
|
|
114
|
+
spans = []
|
|
115
|
+
for n, start in enumerate(headers):
|
|
116
|
+
if not re.match(r"\[\[\s*hooks\s*\]\]\s*(#.*)?\Z", lines[start].rstrip("\r\n")):
|
|
117
|
+
continue
|
|
118
|
+
end = headers[n + 1] if n + 1 < len(headers) else len(lines)
|
|
119
|
+
while end > start + 1 and (not lines[end - 1].strip() or lines[end - 1].lstrip().startswith("#")):
|
|
120
|
+
end -= 1
|
|
121
|
+
try:
|
|
122
|
+
table = tomllib.loads("".join(lines[start:end]))["hooks"][0]
|
|
123
|
+
except (ValueError, KeyError, IndexError, TypeError):
|
|
124
|
+
continue
|
|
125
|
+
if isinstance(table, dict) and _is_ours(table.get("command"), host):
|
|
126
|
+
spans.append((start, end))
|
|
127
|
+
return spans
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _kimi_edit(text: str, host: str, command: Optional[str]) -> str:
|
|
131
|
+
"""`text` with our one `[[hooks]]` table pointed at `command`, or taken out (None) with the comment lines and the
|
|
132
|
+
blank line `_kimi_block` put before it. Refused when there is not exactly one."""
|
|
133
|
+
spans = _kimi_spans(text, host)
|
|
134
|
+
if len(spans) != 1:
|
|
135
|
+
raise Refused("found %d Fabric hook tables to edit; remove the extra ones by hand" % len(spans))
|
|
136
|
+
lines = text.splitlines(True)
|
|
137
|
+
start, end = spans[0]
|
|
138
|
+
if command is not None:
|
|
139
|
+
found = [i for i in range(start + 1, end) if re.match(r"\s*command\s*=", lines[i])]
|
|
140
|
+
if len(found) != 1:
|
|
141
|
+
raise Refused("the Fabric hook's command could not be found line by line; edit it by hand")
|
|
142
|
+
lines[found[0]] = "command = %s\n" % json.dumps(command)
|
|
143
|
+
return "".join(lines)
|
|
144
|
+
if lines[max(0, start - len(_KIMI_COMMENT)):start] == _KIMI_COMMENT:
|
|
145
|
+
start -= len(_KIMI_COMMENT)
|
|
146
|
+
if start > 0 and lines[start - 1] == "\n":
|
|
147
|
+
start -= 1
|
|
148
|
+
return "".join(lines[:start] + lines[end:])
|
|
149
|
+
|
|
150
|
+
|
|
104
151
|
def _replace_file(path: Path, data: bytes, seen: Optional[bytes]) -> Optional[Path]:
|
|
105
152
|
"""Back the file up, then write `data` atomically with the file's own mode. Refused when the file changed
|
|
106
153
|
since it was read. Returns the backup."""
|
|
@@ -163,9 +210,14 @@ def install_host_hooks_cli(argv: Sequence[str], env: Optional[Dict[str, str]] =
|
|
|
163
210
|
path = Path(env.get("CODEX_HOME") or str(_home(env) / ".codex")) / "hooks.json"
|
|
164
211
|
else:
|
|
165
212
|
path = _home(env) / ".kimi-code" / "config.toml"
|
|
166
|
-
|
|
213
|
+
script = Path(args.script).expanduser().absolute() if args.script else default_hook_script(env)
|
|
214
|
+
command = hook_command(str(script), args.host)
|
|
167
215
|
shifted = 0 # Codex hook groups after ours, whose position a removal moves
|
|
216
|
+
updated = False # our own hook was there with another command, and is rewritten (FAA-69)
|
|
168
217
|
try:
|
|
218
|
+
if not args.remove and not script.is_file():
|
|
219
|
+
# A hook pointing at nothing fails at every session start; removal needs no script.
|
|
220
|
+
raise Refused("--script %s is not a file; pass the fabric_session.py of a complete installed skill" % script)
|
|
169
221
|
data = _read_bytes(path)
|
|
170
222
|
digest = _digest(data)
|
|
171
223
|
if args.expect_sha256 and args.expect_sha256 != digest:
|
|
@@ -201,37 +253,43 @@ def install_host_hooks_cli(argv: Sequence[str], env: Optional[Dict[str, str]] =
|
|
|
201
253
|
out("install-host-hooks: the Fabric preamble hook is already in %s; nothing to change" % path)
|
|
202
254
|
return 0
|
|
203
255
|
owned[0]["command"] = command
|
|
256
|
+
updated = True
|
|
204
257
|
new = (json.dumps(doc, indent=2) + "\n").encode("utf-8")
|
|
205
258
|
else:
|
|
206
259
|
doc.setdefault("hooks", {}).setdefault("SessionStart", []).append(entry)
|
|
207
260
|
new = (json.dumps(doc, indent=2) + "\n").encode("utf-8")
|
|
208
|
-
change = "remove from
|
|
261
|
+
change = ("remove from" if args.remove else "update in" if updated else "add to") + " hooks.SessionStart"
|
|
209
262
|
shown = json.dumps(entry, indent=2)
|
|
210
263
|
else:
|
|
211
264
|
block = _kimi_block(command)
|
|
212
265
|
text = (data or b"").decode("utf-8", "replace")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
266
|
+
# Our table is found by what it runs (_is_ours), not by its exact text, so a stale path is repaired and a
|
|
267
|
+
# hand-touched table is still removed (FAA-69). A dry run without a TOML reader shows the plain append.
|
|
268
|
+
readable = tomllib is not None or args.apply or args.remove
|
|
269
|
+
owned = [h for h in _kimi_hooks(data) if _is_ours(h.get("command"), args.host)] if readable else []
|
|
270
|
+
if len(owned) > 1:
|
|
271
|
+
raise Refused("multiple Fabric hooks are configured in %s; remove the extra ones by hand" % path)
|
|
217
272
|
if args.remove:
|
|
218
|
-
if not
|
|
273
|
+
if not owned:
|
|
219
274
|
out("install-host-hooks: no Fabric preamble hook in %s; nothing to change" % path)
|
|
220
275
|
return 0
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
new_text = text.replace(block, "", 1)
|
|
224
|
-
elif present:
|
|
276
|
+
new_text = _kimi_edit(text, args.host, None)
|
|
277
|
+
elif owned and owned[0].get("command") == command:
|
|
225
278
|
out("install-host-hooks: the Fabric preamble hook is already in %s; nothing to change" % path)
|
|
226
279
|
return 0
|
|
280
|
+
elif owned:
|
|
281
|
+
new_text = _kimi_edit(text, args.host, command)
|
|
282
|
+
updated = True
|
|
227
283
|
else:
|
|
228
284
|
new_text = text + ("" if not text or text.endswith("\n") else "\n") + block
|
|
229
285
|
new = new_text.encode("utf-8")
|
|
230
286
|
if args.apply or args.remove:
|
|
231
287
|
after = _kimi_hooks(new)
|
|
232
|
-
|
|
288
|
+
mine = [h.get("command") for h in after if _is_ours(h.get("command"), args.host)]
|
|
289
|
+
growth = -1 if args.remove else 0 if updated else 1
|
|
290
|
+
if len(after) != len(_kimi_hooks(data)) + growth or mine != ([] if args.remove else [command]):
|
|
233
291
|
raise Refused("the edited file would not read back as expected; nothing was changed")
|
|
234
|
-
change = "remove from" if args.remove else "append to"
|
|
292
|
+
change = "remove from" if args.remove else "update in" if updated else "append to"
|
|
235
293
|
shown = block.strip("\n")
|
|
236
294
|
if not (args.apply or args.remove):
|
|
237
295
|
out("install-host-hooks: dry run, nothing is written.")
|
|
@@ -248,6 +306,7 @@ def install_host_hooks_cli(argv: Sequence[str], env: Optional[Dict[str, str]] =
|
|
|
248
306
|
return 0
|
|
249
307
|
backup = _replace_file(path, new, data)
|
|
250
308
|
out("install-host-hooks: %s %s %s." % ("removed the Fabric preamble hook from" if args.remove
|
|
309
|
+
else "updated the Fabric preamble hook in" if updated
|
|
251
310
|
else "added the Fabric preamble hook to", path,
|
|
252
311
|
"(backup: %s)" % backup if backup else "(new file)"))
|
|
253
312
|
out("Undo: fabric_session.py install-host-hooks --host %s %s" % (args.host, "--apply" if args.remove else "--remove"))
|
package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py
CHANGED
|
@@ -15,13 +15,17 @@ from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
|
15
15
|
UNREAD_LINE,
|
|
16
16
|
VERSION,
|
|
17
17
|
WORKER_DEADLINE_S,
|
|
18
|
+
_write_private,
|
|
18
19
|
claude_registration,
|
|
19
20
|
fabric_launched,
|
|
20
21
|
fabric_root,
|
|
21
22
|
locate_hub,
|
|
23
|
+
rejoin_wait,
|
|
24
|
+
session_dir,
|
|
22
25
|
)
|
|
23
26
|
from fabric_session_mcp import ( # noqa: E402 - the kit (FAA-65)
|
|
24
27
|
McpStdio,
|
|
28
|
+
NoSessionError,
|
|
25
29
|
_wait_for_session_tools,
|
|
26
30
|
board_unread,
|
|
27
31
|
)
|
|
@@ -36,9 +40,15 @@ WATCH_MAX_BACKOFF_S = 900
|
|
|
36
40
|
def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None,
|
|
37
41
|
deadline_s: float = WORKER_DEADLINE_S) -> Tuple[str, Any]:
|
|
38
42
|
"""One look at the board through `fabric-mcp --rejoin-only`: ("ok", count), ("unavailable", why) when Fabric
|
|
39
|
-
answers but the board cannot be read, ("down", why) when Fabric cannot be reached
|
|
40
|
-
|
|
43
|
+
answers but the board cannot be read, ("down", why) when Fabric cannot be reached, ("wait", seconds) when the
|
|
44
|
+
session rejoined within the last minute (the Stop worker's mark, or this one's): the rejoin budget is shared, so
|
|
45
|
+
no fabric-mcp starts then (FAA-68). No fabric-mcp starts unless the running Fabric advertises session-hooks/1
|
|
46
|
+
(the rejoin). A successful rejoin renews the mark in the session's folder when the hooks made one."""
|
|
41
47
|
env = dict(os.environ if env is None else env)
|
|
48
|
+
directory = session_dir(session_id, env)
|
|
49
|
+
left = rejoin_wait(directory)
|
|
50
|
+
if left > 0:
|
|
51
|
+
return "wait", left
|
|
42
52
|
hub = locate_hub(fabric_root(env=env))
|
|
43
53
|
if hub["state"] != "running":
|
|
44
54
|
return "down", "Fabric is not running (%s)" % hub["reason"]
|
|
@@ -56,10 +66,17 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
56
66
|
"clientInfo": {"name": "fabric-agent-adapter-watch-mail", "version": VERSION}})
|
|
57
67
|
client.notify("notifications/initialized")
|
|
58
68
|
names = _wait_for_session_tools(client, deadline)
|
|
69
|
+
if directory.is_dir(): # the hooks made it, after their launch check; never made here
|
|
70
|
+
try:
|
|
71
|
+
_write_private(directory / "rejoined", "")
|
|
72
|
+
except OSError:
|
|
73
|
+
pass
|
|
59
74
|
if BOARD_STATUS_TOOL not in names:
|
|
60
75
|
return "unavailable", "this Fabric serves no %s" % BOARD_STATUS_TOOL
|
|
61
76
|
unread, said = board_unread(client.request("tools/call", {"name": BOARD_STATUS_TOOL, "arguments": {}}))
|
|
62
77
|
return ("ok", unread) if unread is not None else ("unavailable", "the project board cannot be read (%s)" % said)
|
|
78
|
+
except NoSessionError:
|
|
79
|
+
return "down", "Fabric holds no session for this id"
|
|
63
80
|
except TimeoutError:
|
|
64
81
|
return "down", "fabric-mcp did not join this session in time"
|
|
65
82
|
except Exception as exc: # noqa: BLE001 — a poll never raises
|
|
@@ -72,15 +89,18 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
72
89
|
def watch_mail(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None, interval: float = 60, *,
|
|
73
90
|
poll: Optional[Callable[[], Tuple[str, Any]]] = None, out: Callable[[str], None] = print,
|
|
74
91
|
wait: Callable[[float], bool], stop: Callable[[], bool], rng: Callable[[], float]) -> int:
|
|
75
|
-
"""Poll the board every `interval` seconds (±20 % jitter
|
|
76
|
-
time the unread count rises. While Fabric is down or the board
|
|
77
|
-
to WATCH_MAX_BACKOFF_S.
|
|
92
|
+
"""Poll the board every `interval` seconds (±20 % jitter; the delay after jitter stays within WATCH_MIN_INTERVAL_S
|
|
93
|
+
and WATCH_MAX_BACKOFF_S) and print ONE line each time the unread count rises. While Fabric is down or the board
|
|
94
|
+
unreadable, say so once and back off, doubling up to WATCH_MAX_BACKOFF_S. A look the shared rejoin mark holds back
|
|
95
|
+
("wait") changes nothing and waits it out. `wait(seconds)` returns True when it was told to stop."""
|
|
78
96
|
interval = max(float(WATCH_MIN_INTERVAL_S), float(interval))
|
|
79
97
|
poll = poll or (lambda: poll_unread(session_id, cwd, env))
|
|
80
98
|
last, trouble, backoff = 0, None, interval
|
|
81
99
|
while not stop():
|
|
82
100
|
kind, value = poll()
|
|
83
|
-
if kind == "
|
|
101
|
+
if kind == "wait":
|
|
102
|
+
delay = float(value)
|
|
103
|
+
elif kind == "ok":
|
|
84
104
|
if value > last:
|
|
85
105
|
out(UNREAD_LINE % value)
|
|
86
106
|
last, trouble, backoff = value, None, interval
|
|
@@ -91,7 +111,8 @@ def watch_mail(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None,
|
|
|
91
111
|
out("watch-mail: %s; trying again, backing off up to %d minutes" % (value, WATCH_MAX_BACKOFF_S // 60))
|
|
92
112
|
trouble = kind
|
|
93
113
|
delay = backoff
|
|
94
|
-
if wait
|
|
114
|
+
jittered = delay if kind == "wait" else delay * (0.8 + 0.4 * rng())
|
|
115
|
+
if wait(min(max(jittered, float(WATCH_MIN_INTERVAL_S)), float(WATCH_MAX_BACKOFF_S))):
|
|
95
116
|
break
|
|
96
117
|
return 0
|
|
97
118
|
|