@event4u/agent-config 1.37.0 → 1.39.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/.agent-src/commands/onboard.md +131 -50
- package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
- package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +64 -0
- package/README.md +36 -1
- package/docs/contracts/mcp-cloud-scope.md +182 -0
- package/docs/contracts/mcp-phase-1-scope.md +8 -3
- package/docs/customization.md +45 -0
- package/docs/guidelines/agent-infra/layered-settings.md +54 -17
- package/docs/guidelines/agent-infra/mcp-request-signing.md +4 -0
- package/docs/mcp-server.md +11 -3
- package/docs/setup/mcp-client-config.md +152 -0
- package/docs/setup/mcp-cloud-endpoints.md +109 -0
- package/docs/setup/mcp-cloud-registry-listing.md +99 -0
- package/docs/setup/mcp-cloud-setup.md +152 -0
- package/docs/setup/mcp-r2-bootstrap.md +82 -0
- package/package.json +1 -1
- package/scripts/_lib/agent_settings.py +168 -0
- package/scripts/mcp_parity_smoke.py +146 -0
- package/scripts/pack_mcp_content.py +274 -0
- package/scripts/readme_linter.py +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Live-replay parity smoke — local stdio kernel vs deployed Worker URL.
|
|
2
|
+
|
|
3
|
+
Replays a fixed set of JSON-RPC calls against:
|
|
4
|
+
|
|
5
|
+
1. The local Python loaders (`prompts.py` / `resources.py`) — the
|
|
6
|
+
source-of-truth wire surface.
|
|
7
|
+
2. An HTTP target (typically `wrangler dev` locally, or the deployed
|
|
8
|
+
Cloudflare Worker URL in CI / post-deploy).
|
|
9
|
+
|
|
10
|
+
Diffs the two on a normalised view (signature + release_key + content
|
|
11
|
+
hashes stripped). Exit 0 = parity, 1 = drift.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python scripts/mcp_parity_smoke.py --target http://127.0.0.1:8787
|
|
15
|
+
python scripts/mcp_parity_smoke.py --target https://mcp.example.com
|
|
16
|
+
|
|
17
|
+
Phase 5.1 of `road-to-cloudflare-mcp-hosting.md`. Governed by
|
|
18
|
+
`docs/contracts/mcp-cloud-scope.md` §A0-cloud.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
import urllib.request
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
_SCRIPTS = Path(__file__).resolve().parent
|
|
30
|
+
sys.path.insert(0, str(_SCRIPTS))
|
|
31
|
+
|
|
32
|
+
from mcp_server.prompts import load_all_prompts, to_mcp_prompt_meta # noqa: E402
|
|
33
|
+
from mcp_server.resources import load_all_resources, to_mcp_resource_meta # noqa: E402
|
|
34
|
+
|
|
35
|
+
PAGE_SIZE = 50
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _local_prompts_list() -> dict[str, Any]:
|
|
39
|
+
prompts, _ = load_all_prompts()
|
|
40
|
+
metas = [to_mcp_prompt_meta(p) for p in prompts]
|
|
41
|
+
page = metas[:PAGE_SIZE]
|
|
42
|
+
out: dict[str, Any] = {"prompts": page}
|
|
43
|
+
if len(metas) > PAGE_SIZE:
|
|
44
|
+
out["nextCursor"] = page[-1]["name"]
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _local_resources_list() -> dict[str, Any]:
|
|
49
|
+
resources, _ = load_all_resources()
|
|
50
|
+
metas = [to_mcp_resource_meta(r) for r in resources]
|
|
51
|
+
page = metas[:PAGE_SIZE]
|
|
52
|
+
out: dict[str, Any] = {"resources": page}
|
|
53
|
+
if len(metas) > PAGE_SIZE:
|
|
54
|
+
out["nextCursor"] = page[-1]["uri"]
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _rpc(target: str, method: str, params: dict[str, Any] | None = None) -> Any:
|
|
59
|
+
body = json.dumps(
|
|
60
|
+
{"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}}
|
|
61
|
+
).encode("utf-8")
|
|
62
|
+
req = urllib.request.Request(
|
|
63
|
+
target,
|
|
64
|
+
data=body,
|
|
65
|
+
headers={"content-type": "application/json"},
|
|
66
|
+
method="POST",
|
|
67
|
+
)
|
|
68
|
+
with urllib.request.urlopen(req, timeout=10) as r: # noqa: S310
|
|
69
|
+
resp = json.loads(r.read().decode("utf-8"))
|
|
70
|
+
if "error" in resp:
|
|
71
|
+
raise RuntimeError(f"{method}: {resp['error']}")
|
|
72
|
+
return resp["result"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_prompts(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
76
|
+
out = []
|
|
77
|
+
for p in payload.get("prompts", []):
|
|
78
|
+
out.append({
|
|
79
|
+
"name": p["name"],
|
|
80
|
+
"description": p["description"],
|
|
81
|
+
"kind": p.get("_meta", {}).get("kind"),
|
|
82
|
+
})
|
|
83
|
+
return sorted(out, key=lambda x: x["name"])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_resources(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
87
|
+
out = []
|
|
88
|
+
for r in payload.get("resources", []):
|
|
89
|
+
out.append({
|
|
90
|
+
"uri": r["uri"],
|
|
91
|
+
"name": r["name"],
|
|
92
|
+
"description": r["description"],
|
|
93
|
+
"mimeType": r["mimeType"],
|
|
94
|
+
"kind": r.get("_meta", {}).get("kind"),
|
|
95
|
+
})
|
|
96
|
+
return sorted(out, key=lambda x: x["uri"])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _diff(label: str, local: list[Any], remote: list[Any]) -> int:
|
|
100
|
+
if local == remote:
|
|
101
|
+
print(f"✅ {label}: {len(local)} entries match")
|
|
102
|
+
return 0
|
|
103
|
+
print(f"❌ {label}: drift ({len(local)} local vs {len(remote)} remote)")
|
|
104
|
+
local_set = {json.dumps(x, sort_keys=True) for x in local}
|
|
105
|
+
remote_set = {json.dumps(x, sort_keys=True) for x in remote}
|
|
106
|
+
only_local = local_set - remote_set
|
|
107
|
+
only_remote = remote_set - local_set
|
|
108
|
+
for s in sorted(only_local)[:5]:
|
|
109
|
+
print(f" local-only: {s}")
|
|
110
|
+
for s in sorted(only_remote)[:5]:
|
|
111
|
+
print(f" remote-only: {s}")
|
|
112
|
+
if len(only_local) > 5 or len(only_remote) > 5:
|
|
113
|
+
print(f" (+{len(only_local) - 5} local, +{len(only_remote) - 5} remote more)")
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main() -> int:
|
|
118
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
119
|
+
ap.add_argument("--target", required=True, help="HTTP URL of the Worker.")
|
|
120
|
+
args = ap.parse_args()
|
|
121
|
+
|
|
122
|
+
failed = 0
|
|
123
|
+
local_p = _normalize_prompts(_local_prompts_list())
|
|
124
|
+
remote_p = _normalize_prompts(_rpc(args.target, "prompts/list"))
|
|
125
|
+
failed += _diff("prompts/list", local_p, remote_p)
|
|
126
|
+
|
|
127
|
+
local_r = _normalize_resources(_local_resources_list())
|
|
128
|
+
remote_r = _normalize_resources(_rpc(args.target, "resources/list"))
|
|
129
|
+
failed += _diff("resources/list", local_r, remote_r)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
_ = _rpc(args.target, "tools/list")
|
|
133
|
+
print("✅ tools/list: round-trips (stub list — content not parity-checked)")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"❌ tools/list: {e}")
|
|
136
|
+
failed += 1
|
|
137
|
+
|
|
138
|
+
if failed:
|
|
139
|
+
print(f"\n{failed} surface(s) drifted between local stdio and {args.target}")
|
|
140
|
+
return 1
|
|
141
|
+
print(f"\nparity OK against {args.target}")
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
sys.exit(main())
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Pack agent-config content into a Worker-bundle JSON blob.
|
|
2
|
+
|
|
3
|
+
Walks `.agent-src/skills/`, `.agent-src/commands/`, `.agent-src/rules/`,
|
|
4
|
+
`docs/guidelines/`, `.agent-src/contexts/` via the same Python loaders
|
|
5
|
+
that drive the local stdio kernel, emits one JSON blob and a sidecar
|
|
6
|
+
manifest for `workers/mcp/`.
|
|
7
|
+
|
|
8
|
+
Outputs (relative to repo root):
|
|
9
|
+
- `workers/mcp/content.json` — uncompressed, bundled by `wrangler deploy`.
|
|
10
|
+
- `workers/mcp/content.json.gz` — gzipped archival copy for R2.
|
|
11
|
+
- `workers/mcp/manifest.json` — manifest only (RCA / R2 sidecar).
|
|
12
|
+
|
|
13
|
+
Hard-fail thresholds (Phase 2-5 council verdict D2):
|
|
14
|
+
- Uncompressed JSON > 2 MB → SystemExit(1).
|
|
15
|
+
- Empty content (zero URIs) → SystemExit(2). Catches a broken
|
|
16
|
+
`.agent-src/` tree before deploy.
|
|
17
|
+
|
|
18
|
+
Cloud signature divergence vs local kernel (`metadata.compute_skill_set_signature`):
|
|
19
|
+
- Local kernel: SHA-256 over `(uri, mtime)` pairs — reproducible only
|
|
20
|
+
within one filesystem.
|
|
21
|
+
- This packer: SHA-256 over `(uri, body)` pairs — reproducible across
|
|
22
|
+
CI runs, machines, and re-clones. Same 12-char prefix.
|
|
23
|
+
|
|
24
|
+
Governed by `docs/contracts/mcp-cloud-scope.md` §A0-cloud invariant 5.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import gzip
|
|
30
|
+
import hashlib
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
# Re-use the local kernel's loaders so the live-replay baseline stays
|
|
40
|
+
# trivially comparable.
|
|
41
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
42
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
43
|
+
|
|
44
|
+
from mcp_server.prompts import scan_commands, scan_skills # noqa: E402
|
|
45
|
+
from mcp_server.resources import scan_contexts, scan_guidelines, scan_rules # noqa: E402
|
|
46
|
+
|
|
47
|
+
SCHEMA_VERSION = 1
|
|
48
|
+
PACKER_VERSION = "1.0.0"
|
|
49
|
+
# Worker bundle is the compact JSON; gzipped copy lives in R2. Cloudflare's
|
|
50
|
+
# compressed-bundle limit is 3 MB (free) / 10 MB (paid); 778 KB gz today
|
|
51
|
+
# (438 entries) leaves ample headroom. Hard-fail at 5 MB uncompressed so
|
|
52
|
+
# the build dies before the Worker upload does.
|
|
53
|
+
MAX_UNCOMPRESSED_BYTES = 5 * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _repo_root() -> Path:
|
|
57
|
+
return Path(__file__).resolve().parent.parent
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _git_sha(root: Path) -> str:
|
|
61
|
+
"""Resolve HEAD SHA. Falls back to env var, then to all-zeros."""
|
|
62
|
+
for env_var in ("GITHUB_SHA", "CI_COMMIT_SHA", "GIT_COMMIT"):
|
|
63
|
+
sha = os.environ.get(env_var)
|
|
64
|
+
if sha and len(sha) >= 7:
|
|
65
|
+
return sha
|
|
66
|
+
try:
|
|
67
|
+
out = subprocess.run(
|
|
68
|
+
["git", "rev-parse", "HEAD"],
|
|
69
|
+
cwd=root,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
check=True,
|
|
73
|
+
timeout=5,
|
|
74
|
+
)
|
|
75
|
+
return out.stdout.strip()
|
|
76
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
77
|
+
return "0" * 40
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _package_version(root: Path) -> str:
|
|
81
|
+
data = json.loads((root / "package.json").read_text(encoding="utf-8"))
|
|
82
|
+
return str(data.get("version", "0.0.0"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _collect_entries(root: Path) -> tuple[dict[str, dict[str, Any]], list[str]]:
|
|
86
|
+
"""Run all 5 scanners and project entries into the wire shape."""
|
|
87
|
+
uris: dict[str, dict[str, Any]] = {}
|
|
88
|
+
errors: list[str] = []
|
|
89
|
+
|
|
90
|
+
skills, e = scan_skills(root)
|
|
91
|
+
errors.extend(e)
|
|
92
|
+
for s in skills:
|
|
93
|
+
key = f"skill://{s.name}"
|
|
94
|
+
uris[key] = {
|
|
95
|
+
"uri": key,
|
|
96
|
+
"name": s.name,
|
|
97
|
+
"description": s.description,
|
|
98
|
+
"body": s.body,
|
|
99
|
+
"source": s.source,
|
|
100
|
+
"kind": "skill",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
commands, e = scan_commands(root)
|
|
104
|
+
errors.extend(e)
|
|
105
|
+
for c in commands:
|
|
106
|
+
key = f"command://{c.name.replace(':', '.')}"
|
|
107
|
+
uris[key] = {
|
|
108
|
+
"uri": key,
|
|
109
|
+
"name": c.name,
|
|
110
|
+
"description": c.description,
|
|
111
|
+
"body": c.body,
|
|
112
|
+
"source": c.source,
|
|
113
|
+
"kind": "command",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for scan in (scan_rules, scan_guidelines, scan_contexts):
|
|
117
|
+
items, e = scan(root)
|
|
118
|
+
errors.extend(e)
|
|
119
|
+
for r in items:
|
|
120
|
+
uris[r.uri] = {
|
|
121
|
+
"uri": r.uri,
|
|
122
|
+
"name": r.name,
|
|
123
|
+
"description": r.description,
|
|
124
|
+
"body": r.body,
|
|
125
|
+
"source": r.source,
|
|
126
|
+
"kind": r.kind,
|
|
127
|
+
"mime_type": r.mime_type,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return uris, errors
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _content_signature(uris: dict[str, dict[str, Any]]) -> tuple[str, str]:
|
|
134
|
+
"""SHA-256 over sorted (uri, body) pairs.
|
|
135
|
+
|
|
136
|
+
Returns (full_hex, 12-char prefix). The prefix is the wire-surface
|
|
137
|
+
`skillSetSignature`; the full hex is the diagnostic `content_hash_sha256`.
|
|
138
|
+
"""
|
|
139
|
+
hasher = hashlib.sha256()
|
|
140
|
+
for uri in sorted(uris):
|
|
141
|
+
hasher.update(uri.encode("utf-8"))
|
|
142
|
+
hasher.update(b"\x00")
|
|
143
|
+
hasher.update(uris[uri]["body"].encode("utf-8"))
|
|
144
|
+
hasher.update(b"\x1e")
|
|
145
|
+
digest = hasher.hexdigest()
|
|
146
|
+
return digest, digest[:12]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _count_kinds(uris: dict[str, dict[str, Any]]) -> dict[str, int]:
|
|
150
|
+
counts = {"skill": 0, "command": 0, "rule": 0, "guideline": 0, "context": 0}
|
|
151
|
+
for entry in uris.values():
|
|
152
|
+
kind = entry["kind"]
|
|
153
|
+
if kind in counts:
|
|
154
|
+
counts[kind] += 1
|
|
155
|
+
return counts
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _build_manifest(
|
|
160
|
+
*,
|
|
161
|
+
signature: str,
|
|
162
|
+
content_hash: str,
|
|
163
|
+
package_version: str,
|
|
164
|
+
git_sha: str,
|
|
165
|
+
built_at: str,
|
|
166
|
+
counts: dict[str, int],
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
short = git_sha[:7] if git_sha and git_sha != "0" * 40 else "unknown"
|
|
169
|
+
return {
|
|
170
|
+
"schema_version": SCHEMA_VERSION,
|
|
171
|
+
"signature": signature,
|
|
172
|
+
"content_hash_sha256": content_hash,
|
|
173
|
+
"package_version": package_version,
|
|
174
|
+
"release_key": f"v{package_version}-{short}",
|
|
175
|
+
"git_sha": git_sha,
|
|
176
|
+
"built_at": built_at,
|
|
177
|
+
"packer_version": PACKER_VERSION,
|
|
178
|
+
"content_uri_count": counts,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def pack(root: Path, out_dir: Path) -> dict[str, Any]:
|
|
183
|
+
"""Run the full pack. Returns the manifest dict."""
|
|
184
|
+
uris, errors = _collect_entries(root)
|
|
185
|
+
if not uris:
|
|
186
|
+
sys.stderr.write("pack: empty content (zero URIs)\n")
|
|
187
|
+
for line in errors:
|
|
188
|
+
sys.stderr.write(f" - {line}\n")
|
|
189
|
+
raise SystemExit(2)
|
|
190
|
+
|
|
191
|
+
content_hash, signature = _content_signature(uris)
|
|
192
|
+
counts = _count_kinds(uris)
|
|
193
|
+
built_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
194
|
+
manifest = _build_manifest(
|
|
195
|
+
signature=signature,
|
|
196
|
+
content_hash=content_hash,
|
|
197
|
+
package_version=_package_version(root),
|
|
198
|
+
git_sha=_git_sha(root),
|
|
199
|
+
built_at=built_at,
|
|
200
|
+
counts=counts,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
blob = {
|
|
204
|
+
"schema_version": SCHEMA_VERSION,
|
|
205
|
+
"uris": uris,
|
|
206
|
+
"manifest": manifest,
|
|
207
|
+
}
|
|
208
|
+
# Compact JSON for the bundle (saves ~20 KB vs indent=2). The R2
|
|
209
|
+
# archival copy is gzipped, so legibility there is moot.
|
|
210
|
+
payload = json.dumps(blob, ensure_ascii=False, sort_keys=True)
|
|
211
|
+
payload_bytes = payload.encode("utf-8")
|
|
212
|
+
|
|
213
|
+
if len(payload_bytes) > MAX_UNCOMPRESSED_BYTES:
|
|
214
|
+
sys.stderr.write(
|
|
215
|
+
f"pack: uncompressed content {len(payload_bytes)} bytes "
|
|
216
|
+
f"exceeds limit {MAX_UNCOMPRESSED_BYTES}\n"
|
|
217
|
+
)
|
|
218
|
+
raise SystemExit(1)
|
|
219
|
+
|
|
220
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
(out_dir / "content.json").write_bytes(payload_bytes)
|
|
222
|
+
# mtime=0 keeps the gzip header byte-stable across CI runs so the
|
|
223
|
+
# R2 archival copy hashes deterministically.
|
|
224
|
+
with open(out_dir / "content.json.gz", "wb") as raw:
|
|
225
|
+
with gzip.GzipFile(
|
|
226
|
+
fileobj=raw, mode="wb", compresslevel=9, mtime=0
|
|
227
|
+
) as gz:
|
|
228
|
+
gz.write(payload_bytes)
|
|
229
|
+
(out_dir / "manifest.json").write_text(
|
|
230
|
+
json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
|
231
|
+
encoding="utf-8",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if errors:
|
|
235
|
+
sys.stderr.write("pack: non-fatal frontmatter errors:\n")
|
|
236
|
+
for line in errors:
|
|
237
|
+
sys.stderr.write(f" - {line}\n")
|
|
238
|
+
|
|
239
|
+
return manifest
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main(argv: list[str] | None = None) -> int:
|
|
243
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
"--root", type=Path, default=_repo_root(), help="Repository root."
|
|
246
|
+
)
|
|
247
|
+
parser.add_argument(
|
|
248
|
+
"--out",
|
|
249
|
+
type=Path,
|
|
250
|
+
default=None,
|
|
251
|
+
help="Output directory (defaults to <root>/workers/mcp).",
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"--quiet", action="store_true", help="Suppress success summary."
|
|
255
|
+
)
|
|
256
|
+
args = parser.parse_args(argv)
|
|
257
|
+
|
|
258
|
+
out_dir = args.out or (args.root / "workers" / "mcp")
|
|
259
|
+
manifest = pack(args.root, out_dir)
|
|
260
|
+
|
|
261
|
+
if not args.quiet:
|
|
262
|
+
c = manifest["content_uri_count"]
|
|
263
|
+
sys.stderr.write(
|
|
264
|
+
f"pack: ok signature={manifest['signature']} "
|
|
265
|
+
f"release={manifest['release_key']} "
|
|
266
|
+
f"skills={c['skill']} commands={c['command']} "
|
|
267
|
+
f"rules={c['rule']} guidelines={c['guideline']} "
|
|
268
|
+
f"contexts={c['context']}\n"
|
|
269
|
+
)
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
raise SystemExit(main())
|