@adia-ai/mcp 0.8.37 → 0.8.39
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 +44 -27
- package/README.md +94 -29
- package/TOOLS.md +178 -16
- package/bin/adia-mcp +17 -8
- package/factory/public-surface.json +20 -0
- package/factory/resources/data-wiring.md +108 -0
- package/factory/resources/pattern-index.md +786 -0
- package/factory/resources/shell-selection.md +86 -0
- package/factory/resources/token-pairing-laws.md +68 -0
- package/factory/server.d.ts +12 -0
- package/factory/server.js +72 -0
- package/factory/tools/factory.js +277 -0
- package/factory/vendor/MANIFEST.json +29 -0
- package/factory/vendor/adia-contract-check.mjs +432 -0
- package/factory/vendor/adia-info +312 -0
- package/factory/vendor/adia-lint +396 -0
- package/factory/vendor/adia-probe.mjs +413 -0
- package/factory/vendor/adia-scaffold +801 -0
- package/factory/vendor/record-lint +278 -0
- package/gen-ui/server.d.ts +1 -1
- package/gen-ui/tools/corpus.js +1 -1
- package/gen-ui/tools/discovery.js +82 -0
- package/gen-ui/tools/feedback.js +1 -1
- package/package.json +13 -3
- package/protocol/server.d.ts +3 -2
- package/protocol/server.js +1 -1
- package/protocol/tools/protocol.js +123 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""adia-info — one-shot project-context probe for adia-ui consumer repos.
|
|
3
|
+
|
|
4
|
+
Prints a compact JSON snapshot of the facts the factory skills otherwise
|
|
5
|
+
re-discover by hand every session: which @adia-ai/* packages the project
|
|
6
|
+
declares (and which version is actually installed), the rendering-mode and
|
|
7
|
+
framework signals app-planning's classifiers cite, which shells the source
|
|
8
|
+
uses, theming setup, MCP wiring, and whether the cwd is actually the
|
|
9
|
+
framework monorepo itself (a misroute signal — that work belongs to the
|
|
10
|
+
adia-ui-forge plugin).
|
|
11
|
+
|
|
12
|
+
Designed for dynamic context injection from a SKILL.md body:
|
|
13
|
+
|
|
14
|
+
!`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/adia-info"`
|
|
15
|
+
|
|
16
|
+
so every field carries the SIGNAL it was derived from (file path or dep
|
|
17
|
+
name) — a skill consuming this output can cite signals per app-planning's
|
|
18
|
+
evidence gate instead of asserting axes bare.
|
|
19
|
+
|
|
20
|
+
Contract: NEVER exits non-zero, never prints to stderr on the happy path —
|
|
21
|
+
a context-injection command that fails or noises breaks the skill it feeds.
|
|
22
|
+
Any probe error degrades that field to null with the error noted in
|
|
23
|
+
`probeErrors`. Bounded: scans at most MAX_SCAN_FILES source files, skipping
|
|
24
|
+
node_modules/dist/.git.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
adia-info [dir] # probe dir (default: cwd); print JSON to stdout
|
|
28
|
+
adia-info [dir] --staleness # ALSO query npm for latest (network; never default)
|
|
29
|
+
adia-info selftest # run built-in fixtures; exit 0 iff all pass
|
|
30
|
+
Stdlib only (Python 3.8+).
|
|
31
|
+
"""
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
MAX_SCAN_FILES = 400
|
|
38
|
+
SOURCE_EXTS = (".html", ".js", ".mjs", ".ts", ".jsx", ".tsx", ".css")
|
|
39
|
+
SKIP_DIRS = {"node_modules", "dist", ".git", ".next", ".nuxt", ".svelte-kit", "build", "coverage"}
|
|
40
|
+
|
|
41
|
+
SSR_FRAMEWORKS = { # dep name -> framework label
|
|
42
|
+
"next": "next", "nuxt": "nuxt", "@sveltejs/kit": "sveltekit", "astro": "astro",
|
|
43
|
+
}
|
|
44
|
+
SHELL_TAGS = ("admin-shell", "chat-shell", "editor-shell", "simple-shell", "embed-shell")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read_json(path):
|
|
48
|
+
try:
|
|
49
|
+
with open(path, encoding="utf-8") as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
except Exception:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _iter_source_files(root):
|
|
56
|
+
count = 0
|
|
57
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
58
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
|
|
59
|
+
for name in filenames:
|
|
60
|
+
if name.endswith(SOURCE_EXTS):
|
|
61
|
+
yield os.path.join(dirpath, name)
|
|
62
|
+
count += 1
|
|
63
|
+
if count >= MAX_SCAN_FILES:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def probe(root):
|
|
68
|
+
info = {"probeRoot": os.path.abspath(root), "probeErrors": []}
|
|
69
|
+
|
|
70
|
+
# ── package.json: adia deps, framework, package manager ──
|
|
71
|
+
pkg = _read_json(os.path.join(root, "package.json")) or {}
|
|
72
|
+
deps = {}
|
|
73
|
+
for key in ("dependencies", "devDependencies"):
|
|
74
|
+
deps.update(pkg.get(key) or {})
|
|
75
|
+
info["adiaPackages"] = {k: v for k, v in sorted(deps.items()) if k.startswith("@adia-ai/")}
|
|
76
|
+
|
|
77
|
+
installed = _read_json(os.path.join(root, "node_modules", "@adia-ai", "web-components", "package.json"))
|
|
78
|
+
info["installedVersion"] = installed.get("version") if installed else None
|
|
79
|
+
|
|
80
|
+
# ── split-lockstep detection (offline): all installed @adia-ai/* versions ──
|
|
81
|
+
# The 11 @adia-ai packages version in lockstep upstream; a consumer whose
|
|
82
|
+
# installed copies diverge (partial upgrade, stale lockfile entry) sees
|
|
83
|
+
# cross-package breakage that presents as component bugs.
|
|
84
|
+
installed_versions = {}
|
|
85
|
+
adia_dir = os.path.join(root, "node_modules", "@adia-ai")
|
|
86
|
+
if os.path.isdir(adia_dir):
|
|
87
|
+
for name in sorted(os.listdir(adia_dir)):
|
|
88
|
+
p = _read_json(os.path.join(adia_dir, name, "package.json"))
|
|
89
|
+
if p and p.get("version"):
|
|
90
|
+
installed_versions[f"@adia-ai/{name}"] = p["version"]
|
|
91
|
+
distinct = sorted(set(installed_versions.values()))
|
|
92
|
+
info["installedAdiaVersions"] = installed_versions or None
|
|
93
|
+
info["splitLockstep"] = (
|
|
94
|
+
{"value": len(distinct) > 1, "versions": distinct,
|
|
95
|
+
"signal": "node_modules/@adia-ai/*/package.json version fields"}
|
|
96
|
+
if installed_versions else None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
framework = next((label for dep, label in SSR_FRAMEWORKS.items() if dep in deps), None)
|
|
100
|
+
if framework is None and "vite" in deps:
|
|
101
|
+
framework = "vite"
|
|
102
|
+
info["framework"] = framework
|
|
103
|
+
|
|
104
|
+
pm = pkg.get("packageManager")
|
|
105
|
+
if pm:
|
|
106
|
+
info["packageManager"] = {"value": pm.split("@")[0], "signal": "package.json packageManager field"}
|
|
107
|
+
else:
|
|
108
|
+
for lockfile, name in (("pnpm-lock.yaml", "pnpm"), ("bun.lockb", "bun"), ("bun.lock", "bun"),
|
|
109
|
+
("yarn.lock", "yarn"), ("package-lock.json", "npm")):
|
|
110
|
+
if os.path.exists(os.path.join(root, lockfile)):
|
|
111
|
+
info["packageManager"] = {"value": name, "signal": lockfile}
|
|
112
|
+
break
|
|
113
|
+
else:
|
|
114
|
+
info["packageManager"] = None
|
|
115
|
+
|
|
116
|
+
# ── framework-monorepo misroute signal ──
|
|
117
|
+
info["isFrameworkMonorepo"] = (
|
|
118
|
+
os.path.isdir(os.path.join(root, "packages", "web-components", "components"))
|
|
119
|
+
and os.path.isdir(os.path.join(root, "packages", "a2ui"))
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# ── rendering mode (app-planning classifier 1, same signals) ──
|
|
123
|
+
if framework in SSR_FRAMEWORKS.values():
|
|
124
|
+
route_dirs = [d for d in ("app", "pages", "src/routes", "src/pages") if os.path.isdir(os.path.join(root, d))]
|
|
125
|
+
info["renderingMode"] = {"value": "ssr", "signal": f"{framework} in package.json" + (f" + {route_dirs[0]}/" if route_dirs else "")}
|
|
126
|
+
elif info["adiaPackages"] or os.path.exists(os.path.join(root, "index.html")):
|
|
127
|
+
info["renderingMode"] = {"value": "spa", "signal": "no SSR framework dep" + ("; index.html present" if os.path.exists(os.path.join(root, "index.html")) else "")}
|
|
128
|
+
else:
|
|
129
|
+
info["renderingMode"] = {"value": "unknown", "signal": "no package.json deps or index.html found"}
|
|
130
|
+
|
|
131
|
+
# ── source scan: shells, registration, theming, custom tags ──
|
|
132
|
+
shells, register_files, theme = set(), [], {"themesCss": False, "dataScheme": False, "namedTheme": False}
|
|
133
|
+
scanned = 0
|
|
134
|
+
try:
|
|
135
|
+
for path in _iter_source_files(root):
|
|
136
|
+
scanned += 1
|
|
137
|
+
try:
|
|
138
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
139
|
+
text = f.read()
|
|
140
|
+
except Exception:
|
|
141
|
+
continue
|
|
142
|
+
for tag in SHELL_TAGS:
|
|
143
|
+
if "<" + tag in text or "adia-" + tag in text:
|
|
144
|
+
shells.add(tag)
|
|
145
|
+
if "@adia-ai/web-components" in text or "@adia-ai/web-modules" in text:
|
|
146
|
+
register_files.append(os.path.relpath(path, root))
|
|
147
|
+
if "themes.css" in text:
|
|
148
|
+
theme["themesCss"] = True
|
|
149
|
+
if "data-scheme" in text:
|
|
150
|
+
theme["dataScheme"] = True
|
|
151
|
+
if re.search(r'\btheme="[a-z]', text):
|
|
152
|
+
theme["namedTheme"] = True
|
|
153
|
+
except Exception as e: # pragma: no cover — never let a scan error kill the probe
|
|
154
|
+
info["probeErrors"].append(f"source scan: {e}")
|
|
155
|
+
info["shellsUsed"] = sorted(shells)
|
|
156
|
+
info["registrationFiles"] = sorted(register_files)[:10]
|
|
157
|
+
info["theme"] = theme
|
|
158
|
+
info["scannedFiles"] = scanned
|
|
159
|
+
|
|
160
|
+
# ── MCP wiring ──
|
|
161
|
+
mcp = _read_json(os.path.join(root, ".mcp.json"))
|
|
162
|
+
servers = (mcp or {}).get("mcpServers") or {}
|
|
163
|
+
a2ui = next((v for k, v in servers.items() if "a2ui" in k), None)
|
|
164
|
+
if a2ui:
|
|
165
|
+
args = " ".join(a2ui.get("args") or [])
|
|
166
|
+
pin = re.search(r"@adia-ai/a2ui-mcp@([\w.\-]+)", args)
|
|
167
|
+
info["a2uiMcp"] = {"configured": True, "pin": pin.group(1) if pin else None}
|
|
168
|
+
else:
|
|
169
|
+
info["a2uiMcp"] = {"configured": False, "pin": None}
|
|
170
|
+
|
|
171
|
+
info["typescript"] = os.path.exists(os.path.join(root, "tsconfig.json"))
|
|
172
|
+
return info
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _selftest():
|
|
176
|
+
import shutil
|
|
177
|
+
import tempfile
|
|
178
|
+
|
|
179
|
+
failures = []
|
|
180
|
+
|
|
181
|
+
def check(name, cond):
|
|
182
|
+
if not cond:
|
|
183
|
+
failures.append(name)
|
|
184
|
+
|
|
185
|
+
# Fixture 1: SPA consumer with adia deps, shells, theme, MCP pin.
|
|
186
|
+
tmp = tempfile.mkdtemp(prefix="adia-info-spa-")
|
|
187
|
+
try:
|
|
188
|
+
with open(os.path.join(tmp, "package.json"), "w") as f:
|
|
189
|
+
json.dump({"dependencies": {"@adia-ai/web-components": "^0.8.0", "vite": "^5.0.0"}}, f)
|
|
190
|
+
with open(os.path.join(tmp, "package-lock.json"), "w") as f:
|
|
191
|
+
f.write("{}")
|
|
192
|
+
with open(os.path.join(tmp, "index.html"), "w") as f:
|
|
193
|
+
f.write('<html data-scheme="dark" theme="ocean"><link href="themes.css">'
|
|
194
|
+
'<script type="module">import "@adia-ai/web-components";</script>'
|
|
195
|
+
"<admin-shell></admin-shell></html>")
|
|
196
|
+
with open(os.path.join(tmp, ".mcp.json"), "w") as f:
|
|
197
|
+
json.dump({"mcpServers": {"a2ui": {"args": ["-y", "@adia-ai/a2ui-mcp@0.8.0"]}}}, f)
|
|
198
|
+
r = probe(tmp)
|
|
199
|
+
check("spa: mode", r["renderingMode"]["value"] == "spa")
|
|
200
|
+
check("spa: adia dep", "@adia-ai/web-components" in r["adiaPackages"])
|
|
201
|
+
check("spa: framework vite", r["framework"] == "vite")
|
|
202
|
+
check("spa: pm npm via lockfile", r["packageManager"] == {"value": "npm", "signal": "package-lock.json"})
|
|
203
|
+
check("spa: shell found", r["shellsUsed"] == ["admin-shell"])
|
|
204
|
+
check("spa: registration file", r["registrationFiles"] == ["index.html"])
|
|
205
|
+
check("spa: theme trio", r["theme"] == {"themesCss": True, "dataScheme": True, "namedTheme": True})
|
|
206
|
+
check("spa: mcp pin", r["a2uiMcp"] == {"configured": True, "pin": "0.8.0"})
|
|
207
|
+
check("spa: not monorepo", r["isFrameworkMonorepo"] is False)
|
|
208
|
+
finally:
|
|
209
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
210
|
+
|
|
211
|
+
# Fixture 2: SSR (next) consumer.
|
|
212
|
+
tmp = tempfile.mkdtemp(prefix="adia-info-ssr-")
|
|
213
|
+
try:
|
|
214
|
+
with open(os.path.join(tmp, "package.json"), "w") as f:
|
|
215
|
+
json.dump({"dependencies": {"next": "15.0.0", "@adia-ai/web-components": "^0.8.0"},
|
|
216
|
+
"packageManager": "pnpm@9.0.0"}, f)
|
|
217
|
+
os.makedirs(os.path.join(tmp, "app"))
|
|
218
|
+
r = probe(tmp)
|
|
219
|
+
check("ssr: mode", r["renderingMode"]["value"] == "ssr")
|
|
220
|
+
check("ssr: signal cites next", "next" in r["renderingMode"]["signal"])
|
|
221
|
+
check("ssr: pm field wins", r["packageManager"]["value"] == "pnpm")
|
|
222
|
+
check("ssr: mcp unconfigured", r["a2uiMcp"]["configured"] is False)
|
|
223
|
+
finally:
|
|
224
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
225
|
+
|
|
226
|
+
# Fixture 3: empty dir never crashes, mode unknown.
|
|
227
|
+
tmp = tempfile.mkdtemp(prefix="adia-info-empty-")
|
|
228
|
+
try:
|
|
229
|
+
r = probe(tmp)
|
|
230
|
+
check("empty: mode unknown", r["renderingMode"]["value"] == "unknown")
|
|
231
|
+
check("empty: no errors", r["probeErrors"] == [])
|
|
232
|
+
json.dumps(r) # must be serializable
|
|
233
|
+
finally:
|
|
234
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
235
|
+
|
|
236
|
+
# Fixture 4 (gh#1122, REQ-06): -h/--help must print usage and exit 0 —
|
|
237
|
+
# NEVER silently probe a literal "-h" directory and return a degenerate
|
|
238
|
+
# JSON payload with exit 0 (the actual repro this fixture locks shut).
|
|
239
|
+
import contextlib
|
|
240
|
+
import io
|
|
241
|
+
for flag in ("-h", "--help"):
|
|
242
|
+
buf = io.StringIO()
|
|
243
|
+
with contextlib.redirect_stdout(buf):
|
|
244
|
+
rc = main(["adia-info", flag])
|
|
245
|
+
out = buf.getvalue()
|
|
246
|
+
check(f"help: {flag} exits 0", rc == 0)
|
|
247
|
+
check(f"help: {flag} prints usage", out.startswith("usage:"))
|
|
248
|
+
check(f"help: {flag} is not a JSON probe payload", "probeRoot" not in out)
|
|
249
|
+
|
|
250
|
+
if failures:
|
|
251
|
+
print("selftest FAIL: " + ", ".join(failures), file=sys.stderr)
|
|
252
|
+
return 1
|
|
253
|
+
print("selftest OK — 4 fixtures")
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _probe_staleness(info):
|
|
258
|
+
"""Opt-in (--staleness): compare installed web-components against npm latest.
|
|
259
|
+
|
|
260
|
+
Network + subprocess, so NEVER part of the default probe — the default
|
|
261
|
+
output feeds skill-load context injection and must stay offline-fast.
|
|
262
|
+
Fail-soft like every other field."""
|
|
263
|
+
import subprocess
|
|
264
|
+
try:
|
|
265
|
+
out = subprocess.run(
|
|
266
|
+
["npm", "view", "@adia-ai/web-components", "version"],
|
|
267
|
+
capture_output=True, text=True, timeout=10,
|
|
268
|
+
)
|
|
269
|
+
if out.returncode != 0:
|
|
270
|
+
raise RuntimeError((out.stderr or out.stdout or "npm view exited nonzero").strip())
|
|
271
|
+
latest = out.stdout.strip() or None
|
|
272
|
+
info["npmLatest"] = latest
|
|
273
|
+
installed = info.get("installedVersion")
|
|
274
|
+
info["staleInstall"] = (
|
|
275
|
+
{"value": installed != latest, "installed": installed, "latest": latest,
|
|
276
|
+
"signal": "npm view @adia-ai/web-components version"}
|
|
277
|
+
if latest and installed else None
|
|
278
|
+
)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
info["npmLatest"] = None
|
|
281
|
+
info["staleInstall"] = None
|
|
282
|
+
info["probeErrors"].append(f"staleness: {e}")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main(argv):
|
|
286
|
+
rest = argv[1:]
|
|
287
|
+
# REQ-06 (gh#1122): -h/--help must print usage and exit 0 BEFORE any
|
|
288
|
+
# positional/root resolution — the bug was that only "--"-prefixed
|
|
289
|
+
# tokens were filtered into `flags`, so single-dash "-h" fell through
|
|
290
|
+
# into `args` and became `root`, silently probing a literal directory
|
|
291
|
+
# named "-h". Checked first, in any position, so it can never fall
|
|
292
|
+
# through regardless of what else is on the command line.
|
|
293
|
+
if any(a in ("-h", "--help") for a in rest):
|
|
294
|
+
print("usage: adia-info [dir] [--staleness] probe an adia-ui consumer app (default: cwd)")
|
|
295
|
+
return 0
|
|
296
|
+
if rest and rest[0] == "selftest":
|
|
297
|
+
return _selftest()
|
|
298
|
+
args = [a for a in rest if not a.startswith("-")]
|
|
299
|
+
flags = {a for a in rest if a.startswith("-")}
|
|
300
|
+
root = args[0] if args else "."
|
|
301
|
+
try:
|
|
302
|
+
info = probe(root)
|
|
303
|
+
if "--staleness" in flags:
|
|
304
|
+
_probe_staleness(info)
|
|
305
|
+
print(json.dumps(info, indent=1))
|
|
306
|
+
except Exception as e: # ultimate fail-soft: emit the error as JSON, exit 0
|
|
307
|
+
print(json.dumps({"probeErrors": [str(e)]}))
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
sys.exit(main(sys.argv))
|