@junghanacs/entwurf 0.14.2 → 0.15.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.
Files changed (87) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +149 -0
  3. package/DELIVERY.md +72 -59
  4. package/README.md +120 -20
  5. package/VERIFY.md +10 -7
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +2 -2
  8. package/docs/external-mcp-host.md +26 -4
  9. package/docs/setup-clean-host.md +118 -18
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
  11. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
  17. package/mcp/entwurf-bridge/src/index.ts +10 -6
  18. package/mcp/entwurf-bridge/tsconfig.build.json +2 -1
  19. package/package.json +22 -11
  20. package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
  21. package/pi/entwurf-capabilities.json +1 -0
  22. package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
  23. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
  24. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
  25. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
  26. package/pi-extensions/entwurf-control.ts +17 -8
  27. package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
  28. package/pi-extensions/lib/meta-sender-identity.ts +10 -2
  29. package/pi-extensions/lib/meta-session.ts +102 -29
  30. package/pi-extensions/lib/mux-fresh-call.ts +82 -11
  31. package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
  32. package/run.sh +1079 -186
  33. package/scripts/agy-bridge.sh +4 -2
  34. package/scripts/check-copilot-birth-hook.ts +1174 -0
  35. package/scripts/check-copilot-launch.ts +395 -0
  36. package/scripts/check-copilot-receive-arm.ts +734 -0
  37. package/scripts/check-copilot-statusline.ts +122 -0
  38. package/scripts/check-entwurf-capabilities.ts +25 -6
  39. package/scripts/check-fresh-cut-gate.sh +104 -27
  40. package/scripts/check-gate-qualification.ts +10 -1
  41. package/scripts/check-install-container.sh +2 -2
  42. package/scripts/check-install-surface.ts +1 -1
  43. package/scripts/check-meta-capability-source.ts +89 -5
  44. package/scripts/check-meta-doctor-oracle.sh +26 -0
  45. package/scripts/check-meta-manifest-schema.py +38 -0
  46. package/scripts/check-meta-receiver-marker.ts +25 -2
  47. package/scripts/check-meta-session.ts +32 -0
  48. package/scripts/check-setup-qualification.sh +126 -0
  49. package/scripts/copilot-bridge-doctor.sh +242 -0
  50. package/scripts/copilot-bridge-install.sh +384 -0
  51. package/scripts/copilot-bridge-oracle.sh +223 -0
  52. package/scripts/copilot-bridge-uninstall.sh +137 -0
  53. package/scripts/copilot-launch.sh +236 -0
  54. package/scripts/copilot-mcp-bridge.sh +177 -0
  55. package/scripts/copilot-mcp-config.py +221 -0
  56. package/scripts/copilot-receive-bridge.sh +483 -0
  57. package/scripts/copilot-statusline-bridge.sh +149 -0
  58. package/scripts/copilot-statusline-config.py +201 -0
  59. package/scripts/copilot-statusline.sh +68 -0
  60. package/scripts/dev-bin.sh +53 -9
  61. package/scripts/fake-copilot-vendor.sh +67 -0
  62. package/scripts/meta-bridge-hook-log.sh +17 -5
  63. package/scripts/mutants/capability-cache.json +64 -0
  64. package/scripts/mutants/copilot-birth.json +244 -0
  65. package/scripts/mutants/copilot-launch.json +187 -0
  66. package/scripts/mutants/copilot-receive.json +263 -0
  67. package/scripts/mutants/fresh-cut.json +17 -0
  68. package/scripts/mutants/mux-fresh-call.json +216 -2
  69. package/scripts/mutants/pack-install.json +17 -0
  70. package/scripts/mutants/pi-package-ownership.json +92 -0
  71. package/scripts/mutants/setup-verdict.json +148 -0
  72. package/scripts/mutants/source-install.json +32 -0
  73. package/scripts/pi_settings_io.py +22 -0
  74. package/scripts/raw-async-delivery/README.md +170 -125
  75. package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
  76. package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
  77. package/scripts/register-pi-package.py +525 -7
  78. package/scripts/register-pi-provider.py +162 -12
  79. package/scripts/smoke-agy-install-state.sh +86 -30
  80. package/scripts/smoke-agy-statusline-state.sh +9 -6
  81. package/scripts/smoke-copilot-mcp-state.sh +153 -0
  82. package/scripts/smoke-copilot-statusline-state.sh +131 -0
  83. package/scripts/smoke-mux-fresh-call-live.ts +2 -0
  84. package/scripts/smoke-mux-lifecycle-live.ts +3 -1
  85. package/scripts/smoke-setup-verdict.sh +291 -0
  86. package/scripts/smoke-user-scope-citizen.sh +388 -1
  87. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+ """Stateful Copilot MCP-config adapter; stdlib only.
3
+
4
+ Owns ONE server key (``entwurf-bridge``) inside ``~/.copilot/mcp-config.json``.
5
+ The file wrapper is always ``mcpServers``. The stdio entry follows Copilot CLI's
6
+ writer, not the API wire schema: ``{type:"local", command, args?}``.
7
+ Adopts regular files, refuses symlinks, records first-install preimages.
8
+ """
9
+
10
+ import datetime
11
+ import json
12
+ import os
13
+ import sys
14
+ import tempfile
15
+
16
+ STATE_SCHEMA_VERSION = 1
17
+ SERVER_KEY = "entwurf-bridge"
18
+ ENTRY_TYPE = "local"
19
+
20
+
21
+ def die(code: int, message: str) -> "None":
22
+ sys.stderr.write(message.rstrip("\n") + "\n")
23
+ raise SystemExit(code)
24
+
25
+
26
+ def load_object(path: str, label: str) -> dict:
27
+ try:
28
+ with open(path, "r", encoding="utf-8") as handle:
29
+ raw = handle.read()
30
+ except OSError as error:
31
+ die(4, f"copilot-mcp: cannot read {label} {path}: {error}")
32
+ if not raw.strip():
33
+ return {}
34
+ try:
35
+ value = json.loads(raw)
36
+ except json.JSONDecodeError as error:
37
+ die(4, f"copilot-mcp: {label} {path} is not valid JSON: {error}")
38
+ if not isinstance(value, dict):
39
+ die(4, f"copilot-mcp: {label} {path} top-level must be a JSON object")
40
+ return value
41
+
42
+
43
+ def atomic_write(path: str, value: dict) -> None:
44
+ directory = os.path.dirname(os.path.abspath(path))
45
+ os.makedirs(directory, exist_ok=True)
46
+ fd, temporary = tempfile.mkstemp(dir=directory, prefix=".copilot-mcp-", suffix=".json")
47
+ try:
48
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
49
+ json.dump(value, handle, indent=2)
50
+ handle.write("\n")
51
+ os.replace(temporary, path)
52
+ except BaseException:
53
+ try:
54
+ os.unlink(temporary)
55
+ except FileNotFoundError:
56
+ pass
57
+ raise
58
+
59
+
60
+ def now() -> str:
61
+ return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
62
+
63
+
64
+ def our_entry(command: str) -> dict:
65
+ # CLI `copilot mcp add` writes type:"local" (not "stdio") plus command/args.
66
+ # args is present and empty so the inverse has a stable shape; env/timeout/tools stay omitted.
67
+ return {"type": ENTRY_TYPE, "command": command, "args": []}
68
+
69
+
70
+ def checked_state(state_path: str) -> dict:
71
+ state = load_object(state_path, "install-state")
72
+ required = (
73
+ "managedConfigPath",
74
+ "serverKey",
75
+ "command",
76
+ "detectMode",
77
+ "configExistedBefore",
78
+ "preimage",
79
+ )
80
+ if state.get("schemaVersion") != STATE_SCHEMA_VERSION or any(key not in state for key in required):
81
+ die(4, f"copilot-mcp: install-state {state_path} has an unsupported shape")
82
+ if state.get("serverKey") != SERVER_KEY:
83
+ die(4, f"copilot-mcp: install-state {state_path} serverKey is not {SERVER_KEY}")
84
+ return state
85
+
86
+
87
+ def prior_state(state_path: str, config_path: str) -> dict | None:
88
+ if not os.path.exists(state_path):
89
+ return None
90
+ state = checked_state(state_path)
91
+ return state if state["managedConfigPath"] == os.path.abspath(config_path) else None
92
+
93
+
94
+ def install(config_path: str, command: str, state_path: str) -> None:
95
+ if os.path.islink(config_path):
96
+ die(3, f"copilot-mcp: refusing to adopt {config_path} — symlink to {os.readlink(config_path)} (someone else's SSOT)")
97
+
98
+ existed = os.path.exists(config_path)
99
+ data = load_object(config_path, "config") if existed else {}
100
+ servers = data.get("mcpServers")
101
+ if servers is None:
102
+ servers = {}
103
+ data["mcpServers"] = servers
104
+ if not isinstance(servers, dict):
105
+ die(4, f"copilot-mcp: {config_path} mcpServers must be a JSON object")
106
+
107
+ prior = prior_state(state_path, config_path)
108
+ if prior is None:
109
+ state = {
110
+ "schemaVersion": STATE_SCHEMA_VERSION,
111
+ "managedConfigPath": os.path.abspath(config_path),
112
+ "serverKey": SERVER_KEY,
113
+ "command": command,
114
+ "detectMode": "adopt-regular-file" if existed else "created-new",
115
+ "configExistedBefore": existed,
116
+ "preimage": servers.get(SERVER_KEY),
117
+ "installedAt": now(),
118
+ }
119
+ else:
120
+ state = prior
121
+
122
+ servers[SERVER_KEY] = our_entry(command)
123
+ atomic_write(config_path, data)
124
+ atomic_write(state_path, state)
125
+ sys.stdout.write(f"{state['detectMode']} {os.path.abspath(config_path)}\n")
126
+
127
+
128
+ def uninstall(state_path: str) -> None:
129
+ if not os.path.exists(state_path):
130
+ die(2, f"copilot-mcp: no install-state at {state_path} — nothing to undo")
131
+ state = checked_state(state_path)
132
+ config_path = state.get("managedConfigPath")
133
+ if not isinstance(config_path, str) or not os.path.isabs(config_path):
134
+ die(4, f"copilot-mcp: install-state {state_path} has no absolute managedConfigPath")
135
+ if os.path.islink(config_path):
136
+ die(3, f"copilot-mcp: refusing to uninstall — {config_path} became a symlink (someone else's SSOT)")
137
+
138
+ if os.path.exists(config_path):
139
+ data = load_object(config_path, "config")
140
+ servers = data.get("mcpServers")
141
+ if isinstance(servers, dict):
142
+ preimage = state.get("preimage")
143
+ if preimage is None:
144
+ servers.pop(SERVER_KEY, None)
145
+ else:
146
+ servers[SERVER_KEY] = preimage
147
+ created = state.get("detectMode") == "created-new" and state.get("configExistedBefore") is False
148
+ only_ours = created and len(servers) == 0 and set(data.keys()) == {"mcpServers"}
149
+ if only_ours:
150
+ os.remove(config_path)
151
+ else:
152
+ atomic_write(config_path, data)
153
+
154
+ os.remove(state_path)
155
+ sys.stdout.write(f"uninstalled {config_path}\n")
156
+
157
+
158
+ def _doctor_invocation(config_path: str, command: str):
159
+ if os.path.islink(config_path):
160
+ return "symlink", None
161
+ if not os.path.exists(config_path):
162
+ return "file-absent", None
163
+ try:
164
+ data = load_object(config_path, "config")
165
+ except SystemExit:
166
+ return "invalid-json", None
167
+ servers = data.get("mcpServers")
168
+ if not isinstance(servers, dict) or SERVER_KEY not in servers:
169
+ return "not-ours", None
170
+ server = servers.get(SERVER_KEY)
171
+ if not isinstance(server, dict):
172
+ return "invalid-entry", None
173
+ found = server.get("command")
174
+ args = server.get("args", [])
175
+ env = server.get("env", {})
176
+ if server.get("type") != ENTRY_TYPE:
177
+ return "invalid-entry", None
178
+ if not isinstance(found, str) or not found:
179
+ return "invalid-entry", None
180
+ if found != command:
181
+ return "not-ours", None
182
+ if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args):
183
+ return "invalid-entry", None
184
+ if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()):
185
+ return "invalid-entry", None
186
+ return "configured", {"command": found, "args": args, "env": env}
187
+
188
+
189
+ def doctor_static(config_path: str, command: str) -> None:
190
+ status, invocation = _doctor_invocation(config_path, command)
191
+ if status != "configured":
192
+ sys.stdout.write(f"{status}\n")
193
+ return
194
+ sys.stdout.write(f"configured {invocation['command']}\n")
195
+
196
+
197
+ def doctor_invocation(config_path: str, command: str) -> None:
198
+ status, invocation = _doctor_invocation(config_path, command)
199
+ if status != "configured":
200
+ die(4, f"copilot-mcp: cannot read configured invocation from {config_path}: {status}")
201
+ sys.stdout.write(json.dumps(invocation, separators=(",", ":")) + "\n")
202
+
203
+
204
+ def main(argv: list[str]) -> None:
205
+ if len(argv) < 2:
206
+ die(5, "usage: copilot-mcp-config.py <install|uninstall|doctor-static|doctor-invocation> ...")
207
+ match argv[1]:
208
+ case "install" if len(argv) == 5:
209
+ install(argv[2], argv[3], argv[4])
210
+ case "uninstall" if len(argv) == 3:
211
+ uninstall(argv[2])
212
+ case "doctor-static" if len(argv) == 4:
213
+ doctor_static(argv[2], argv[3])
214
+ case "doctor-invocation" if len(argv) == 4:
215
+ doctor_invocation(argv[2], argv[3])
216
+ case _:
217
+ die(5, "usage: copilot-mcp-config.py <install config command state|uninstall state|doctor-static config command|doctor-invocation config command>")
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main(sys.argv)