@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.99

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 (123) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/LICENSE +92 -0
  3. package/README.md +86 -2
  4. package/bin/hellolynk-client-setup.mjs +2748 -0
  5. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  6. package/hellolynk-client-pack-manifest.json +619 -0
  7. package/hellolynk-client-pack-pin.json +4 -0
  8. package/package.json +58 -4
  9. package/plugins/hellolynk-codex/.app.json +3 -0
  10. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
  11. package/plugins/hellolynk-codex/.mcp.json +19 -0
  12. package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
  13. package/plugins/hellolynk-codex/package.json +11 -0
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  15. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  16. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  17. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  18. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  19. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  20. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  21. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  22. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  23. package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  24. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  34. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  35. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  36. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
  37. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  38. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  39. package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
  40. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  41. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  42. package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  43. package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  44. package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  45. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  46. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  47. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  48. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  49. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  50. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  51. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  52. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  53. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
  54. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
  55. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  56. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
  57. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  58. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  59. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
  60. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  61. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  62. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  63. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  64. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  65. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  66. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  67. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  68. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  69. package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  70. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +160 -0
  71. package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  72. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  73. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  74. package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
  75. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
  76. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
  77. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
  78. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
  79. package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
  80. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
  81. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  82. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  83. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  84. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  85. package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
  86. package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
  87. package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
  88. package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
  89. package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
  90. package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
  91. package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
  92. package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
  93. package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
  94. package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
  95. package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
  96. package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
  97. package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
  98. package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
  99. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  100. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
  101. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
  102. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
  103. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
  104. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
  105. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
  106. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
  107. package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
  108. package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
  109. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  110. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  111. package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
  112. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
  113. package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
  114. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
  115. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
  116. package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
  117. package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
  118. package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
  119. package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
  120. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
  121. package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
  122. package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
  123. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
@@ -0,0 +1,151 @@
1
+ // MCP Apps use JSON-RPC over postMessage for both initialization and calls back
2
+ // to their MCP server. Keeping the entire Workbench on this one channel avoids
3
+ // losing app-only `_meta` when a host also exposes an older compatibility API.
4
+ export function createStandardMcpAppClient(appWindow, options = {}) {
5
+ const positiveTimeout = (value, fallback) => Number.isSafeInteger(value) && value > 0 ? value : fallback;
6
+ const DEFAULT_MCP_BRIDGE_TIMEOUT_MS = 10_000;
7
+ const DEFAULT_MCP_SNAPSHOT_TIMEOUT_MS = 60_000;
8
+ const MCP_APP_PROTOCOL_VERSION = "2026-01-26";
9
+ if (!appWindow || !appWindow.parent || appWindow.parent === appWindow
10
+ || typeof appWindow.parent.postMessage !== "function"
11
+ || typeof appWindow.addEventListener !== "function") {
12
+ throw new Error("workbench_mcp_app_bridge_required");
13
+ }
14
+ const bridgeTimeoutMs = positiveTimeout(options.bridgeTimeoutMs, DEFAULT_MCP_BRIDGE_TIMEOUT_MS);
15
+ const parent = appWindow.parent;
16
+ const pending = new Map();
17
+ const hostContextListeners = new Set();
18
+ const toolResultListeners = new Set();
19
+ let requestSequence = 0;
20
+ let connected = null;
21
+ let closed = false;
22
+
23
+ function post(message) {
24
+ if (closed) throw new Error("workbench_mcp_app_bridge_closed");
25
+ parent.postMessage(message, "*");
26
+ }
27
+
28
+ function notify(method, params) {
29
+ post({
30
+ jsonrpc: "2.0",
31
+ method,
32
+ ...(params === undefined ? {} : { params }),
33
+ });
34
+ }
35
+
36
+ function request(method, params, timeoutMs = bridgeTimeoutMs) {
37
+ if (closed) return Promise.reject(new Error("workbench_mcp_app_bridge_closed"));
38
+ const id = `hellolynk-workbench-${Date.now()}-${++requestSequence}`;
39
+ return new Promise((resolve, reject) => {
40
+ const timeout = setTimeout(() => {
41
+ pending.delete(id);
42
+ const error = new Error("workbench_mcp_app_bridge_timeout");
43
+ error.code = "workbench_mcp_app_bridge_timeout";
44
+ reject(error);
45
+ }, timeoutMs);
46
+ pending.set(id, { resolve, reject, timeout });
47
+ try {
48
+ post({ jsonrpc: "2.0", id, method, params });
49
+ } catch (error) {
50
+ clearTimeout(timeout);
51
+ pending.delete(id);
52
+ reject(error);
53
+ }
54
+ });
55
+ }
56
+
57
+ function onMessage(event) {
58
+ if (event.source !== parent) return;
59
+ const message = event.data;
60
+ if (message?.jsonrpc !== "2.0") return;
61
+ if (!Object.hasOwn(message, "id")) {
62
+ if (message.method === "ui/notifications/tool-result") {
63
+ for (const listener of toolResultListeners) listener(message.params);
64
+ }
65
+ if (message.method === "ui/notifications/host-context-changed"
66
+ && message.params && typeof message.params === "object" && !Array.isArray(message.params)) {
67
+ for (const listener of hostContextListeners) listener(message.params);
68
+ }
69
+ return;
70
+ }
71
+ if (message.method) return;
72
+ const entry = pending.get(message.id);
73
+ if (!entry) return;
74
+ pending.delete(message.id);
75
+ clearTimeout(entry.timeout);
76
+ if (message.error) {
77
+ const code = message.error?.data?.code || message.error?.code || "workbench_mcp_app_bridge_error";
78
+ const error = new Error(String(message.error?.message || code));
79
+ error.code = code;
80
+ entry.reject(error);
81
+ return;
82
+ }
83
+ if (!Object.hasOwn(message, "result")) {
84
+ const error = new Error("workbench_mcp_app_bridge_response_invalid");
85
+ error.code = "workbench_mcp_app_bridge_response_invalid";
86
+ entry.reject(error);
87
+ return;
88
+ }
89
+ entry.resolve(message.result);
90
+ }
91
+
92
+ appWindow.addEventListener("message", onMessage);
93
+
94
+ async function connect() {
95
+ if (connected) return connected;
96
+ const attempt = request("ui/initialize", {
97
+ appInfo: options.appInfo || { name: "HelloLynk Workbench", version: "2" },
98
+ appCapabilities: {},
99
+ protocolVersion: MCP_APP_PROTOCOL_VERSION,
100
+ }).then((result) => {
101
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
102
+ throw new Error("workbench_mcp_app_initialize_invalid");
103
+ }
104
+ notify("ui/notifications/initialized");
105
+ return result;
106
+ });
107
+ connected = attempt.catch((error) => {
108
+ connected = null;
109
+ throw error;
110
+ });
111
+ return connected;
112
+ }
113
+
114
+ return Object.freeze({
115
+ async connect() {
116
+ return connect();
117
+ },
118
+ async callTool(name, args = {}, timeoutMs = DEFAULT_MCP_SNAPSHOT_TIMEOUT_MS) {
119
+ await connect();
120
+ return request("tools/call", { name, arguments: args }, timeoutMs);
121
+ },
122
+ async openLink(url) {
123
+ const session = await connect();
124
+ if (!session?.hostCapabilities?.openLinks) {
125
+ throw new Error("workbench_mcp_open_link_unavailable");
126
+ }
127
+ return request("ui/open-link", { url });
128
+ },
129
+ notify,
130
+ onToolResult(listener) {
131
+ toolResultListeners.add(listener);
132
+ return () => toolResultListeners.delete(listener);
133
+ },
134
+ onHostContextChanged(listener) {
135
+ hostContextListeners.add(listener);
136
+ return () => hostContextListeners.delete(listener);
137
+ },
138
+ close() {
139
+ if (closed) return;
140
+ closed = true;
141
+ appWindow.removeEventListener?.("message", onMessage);
142
+ for (const entry of pending.values()) {
143
+ clearTimeout(entry.timeout);
144
+ entry.reject(new Error("workbench_mcp_app_bridge_closed"));
145
+ }
146
+ pending.clear();
147
+ hostContextListeners.clear();
148
+ toolResultListeners.clear();
149
+ },
150
+ });
151
+ }
@@ -0,0 +1,452 @@
1
+ import { mountWorkbench, publicError } from "./app.mjs";
2
+ import { createStandardMcpAppClient } from "./mcp-app-client.mjs";
3
+
4
+ // A true first launch can spend about 32 seconds verifying the immutable
5
+ // release, resolving the protected Windows binding and starting the local
6
+ // Host. Keep a finite ceiling so a broken bridge cannot spin forever, while
7
+ // still allowing that verified cold-start path to finish.
8
+ const DEFAULT_MCP_SNAPSHOT_TIMEOUT_MS = 60_000;
9
+ const PORTAL_URLS = Object.freeze({
10
+ keys: "https://hellolynk.com/account#keys",
11
+ devices: "https://hellolynk.com/account#devices",
12
+ });
13
+
14
+ function enableMcpAppSizeNotifications(client, initialHostContext = {}) {
15
+ if (window.parent === window) return;
16
+
17
+ // The shared layout fills its viewport. Echoing that viewport's height keeps
18
+ // a host's small bootstrap iframe small forever. Request a useful chat height
19
+ // independently, while leaving fixed and capped containers under host control.
20
+ const preferredHeight = 680;
21
+ let hostContext = initialHostContext;
22
+ let frameRequested = false;
23
+ let lastWidth = 0;
24
+ let lastHeight = 0;
25
+
26
+ function sendSizeChanged() {
27
+ frameRequested = false;
28
+ const width = Math.ceil(document.documentElement.clientWidth || window.innerWidth || 0);
29
+ const dimensions = hostContext?.containerDimensions;
30
+ const positiveDimension = (value) => Number.isFinite(value) && value > 0;
31
+ const height = Math.floor(positiveDimension(dimensions?.height)
32
+ ? dimensions.height
33
+ : positiveDimension(dimensions?.maxHeight)
34
+ ? Math.min(preferredHeight, dimensions.maxHeight)
35
+ : preferredHeight);
36
+ if (!width || !height || (width === lastWidth && height === lastHeight)) return;
37
+ lastWidth = width;
38
+ lastHeight = height;
39
+ client.notify("ui/notifications/size-changed", { width, height });
40
+ }
41
+
42
+ function scheduleSizeChanged() {
43
+ if (frameRequested) return;
44
+ frameRequested = true;
45
+ window.requestAnimationFrame(sendSizeChanged);
46
+ }
47
+
48
+ if (typeof window.ResizeObserver === "function") {
49
+ const observer = new window.ResizeObserver(scheduleSizeChanged);
50
+ observer.observe(document.documentElement);
51
+ observer.observe(document.body);
52
+ }
53
+ window.addEventListener("resize", scheduleSizeChanged);
54
+ client.onHostContextChanged((update) => {
55
+ hostContext = { ...hostContext, ...update };
56
+ scheduleSizeChanged();
57
+ });
58
+ scheduleSizeChanged();
59
+ }
60
+
61
+ function createMcpAppTransport(client, options = {}) {
62
+ if (!client || typeof client.callTool !== "function") throw new Error("workbench_mcp_app_bridge_required");
63
+ const snapshotTimeoutMs = positiveTimeout(options.snapshotTimeoutMs, DEFAULT_MCP_SNAPSHOT_TIMEOUT_MS);
64
+ // Codex's retained global surface offers only fullscreen. Chat cards also
65
+ // offer inline, even while expanded; they must not become background polls.
66
+ const refreshesWhileHidden = options.hostContext?.displayMode === "fullscreen"
67
+ && options.hostContext?.availableDisplayModes?.length === 1
68
+ && options.hostContext.availableDisplayModes[0] === "fullscreen";
69
+ // Chat history can keep several cards mounted under the same capability.
70
+ // Leave room in its replay budget for heartbeats, native inbox reads and
71
+ // human actions; each empty change read is still an authenticated request.
72
+ const changePollMs = positiveTimeout(options.changePollMs, refreshesWhileHidden ? 1_000 : 5_000);
73
+ const pageDocument = typeof document === "object" ? document : null;
74
+ // MCP dispatch can queue this metadata read behind a snapshot or command.
75
+ // Keep its bridge deadline wide enough for that queue; Host changes HTTP is
76
+ // still bounded at eight seconds once dispatched.
77
+ const changeTimeoutMs = positiveTimeout(options.changeTimeoutMs, DEFAULT_MCP_SNAPSHOT_TIMEOUT_MS);
78
+ let accountRevision = null;
79
+ const scoped = (args) => accountRevision === null ? args : { ...args, _hellolynkAccountRevision: accountRevision };
80
+ async function accountCall(name, input, field) {
81
+ const response = await client.callTool(name, input);
82
+ if (response?.isError) throw knownFailure(response?.structuredContent?.errorCode || "workbench_account_unavailable");
83
+ if (!privateOnlyVisibleEnvelope(response)) throw knownFailure("workbench_account_state_invalid");
84
+ const payload = response?._meta?.hellolynkWorkbench;
85
+ if (!payload || Object.keys(payload).join(",") !== field
86
+ || !/^(?:initial|[a-f0-9]{32})$/.test(payload[field]?.revision)) throw knownFailure("workbench_account_state_invalid");
87
+ accountRevision = payload[field].revision;
88
+ return payload[field];
89
+ }
90
+ async function readPrivateSnapshot(args) {
91
+ const response = await withTimeout(
92
+ client.callTool("hellolynk_workbench_snapshot", scoped(args)),
93
+ snapshotTimeoutMs,
94
+ "workbench_mcp_snapshot_timeout",
95
+ );
96
+ if (response?.isError) {
97
+ const code = response?.structuredContent?.errorCode
98
+ || response?.structuredContent?.error
99
+ || response?._meta?.errorCode
100
+ || "workbench_mcp_call_failed";
101
+ const error = new Error(code);
102
+ error.code = code;
103
+ throw error;
104
+ }
105
+ if (!privateOnlyVisibleEnvelope(response)) {
106
+ throw new Error("workbench_mcp_model_visible_snapshot_rejected");
107
+ }
108
+ const payload = response?._meta?.hellolynkWorkbench;
109
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)
110
+ || Object.keys(payload).length !== 1 || !payload.snapshot) {
111
+ throw new Error("workbench_mcp_private_snapshot_missing");
112
+ }
113
+ if (payload.snapshot?.source?.mode !== "neutral-host"
114
+ || payload.snapshot?.source?.transport !== "mcp-app-rpc") {
115
+ throw new Error("workbench_mcp_snapshot_source_mismatch");
116
+ }
117
+ return restoreMcpNullFields(payload.snapshot, [
118
+ "commandWindow", "contactRequestNextCursor",
119
+ "subject.principalLabel", "subject.delegateLabel",
120
+ "threads.nextCursor", "threads.items.*.localDelegateId", "threads.items.*.peerDelegateId",
121
+ "threads.items.*.contactId", "threads.items.*.peerPrincipalId", "threads.items.*.peerHelloLynkId",
122
+ "threads.items.*.delegateLabel", "threads.items.*.lastMessage",
123
+ "messages.threadId", "messages.nextCursor",
124
+ "localHistory.autoArchiveAfterHours", "localHistory.selection", "localHistory.selection.before",
125
+ "messages.items.*.senderPrincipalId", "messages.items.*.senderDelegateId",
126
+ "messages.items.*.intentSource", "messages.items.*.authorizationScope",
127
+ "outbox.*.threadId", "outbox.*.localDelegateId", "outbox.*.recipientDelegateId", "outbox.*.errorCategory",
128
+ "contactRequests.*.senderLabel", "contactRequests.*.otherLabel",
129
+ "contactRequests.*.decisionOutcome", "contactRequests.*.decisionAction",
130
+ "outgoingContactRequests.*.senderLabel", "outgoingContactRequests.*.otherLabel",
131
+ "outgoingContactRequests.*.decisionOutcome", "outgoingContactRequests.*.decisionAction",
132
+ "relationships.*.otherLabel", "relationships.*.endedAt", "blocks.*.otherLabel",
133
+ ]);
134
+ }
135
+ async function executePrivateCommand(command) {
136
+ const actionGrant = command.command === "message.send-status" ? null : await issuePrivateActionGrant(command);
137
+ const response = await client.callTool("hellolynk_workbench_command", scoped({
138
+ command,
139
+ ...(actionGrant ? { _hellolynkUiActionGrant: actionGrant } : {}),
140
+ }), DEFAULT_MCP_SNAPSHOT_TIMEOUT_MS);
141
+ if (response?.isError) {
142
+ const code = response?.structuredContent?.errorCode
143
+ || response?.structuredContent?.error
144
+ || response?._meta?.errorCode
145
+ || "workbench_mcp_call_failed";
146
+ const error = new Error(code);
147
+ error.code = code;
148
+ if (knownPreMutationError(code)) error.outcome = "known-failed";
149
+ throw error;
150
+ }
151
+ if (!privateOnlyVisibleEnvelope(response)) {
152
+ throw new Error("workbench_mcp_model_visible_command_result_rejected");
153
+ }
154
+ const payload = response?._meta?.hellolynkWorkbench;
155
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)
156
+ || Object.keys(payload).length !== 1 || !payload.commandResult) {
157
+ throw new Error("workbench_mcp_private_command_result_missing");
158
+ }
159
+ const nullableFields = ["revision", "changeCursor", "error.retryAfterMs", "error.currentRevision"];
160
+ if (payload.commandResult.result?.kind === "principal-search") {
161
+ nullableFields.push("result.results.*.displayName", "result.results.*.hellolynkId");
162
+ } else if (payload.commandResult.result?.kind === "contact-request-send") {
163
+ nullableFields.push("result.quota");
164
+ } else if (payload.commandResult.result?.kind === "message-send") {
165
+ nullableFields.push("result.code");
166
+ }
167
+ return restoreMcpNullFields(payload.commandResult, nullableFields);
168
+ }
169
+ async function issuePrivateActionGrant(command) {
170
+ const response = await client.callTool("hellolynk_workbench_action_grant", scoped({ command }));
171
+ if (response?.isError) {
172
+ const code = response?.structuredContent?.errorCode
173
+ || response?.structuredContent?.error
174
+ || response?._meta?.errorCode
175
+ || "workbench_mcp_action_grant_failed";
176
+ const error = new Error(code);
177
+ error.code = code;
178
+ error.outcome = "known-failed";
179
+ throw error;
180
+ }
181
+ if (!privateOnlyVisibleEnvelope(response)) {
182
+ throw knownFailure("workbench_mcp_model_visible_action_grant_rejected");
183
+ }
184
+ const payload = response?._meta?.hellolynkWorkbench;
185
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)
186
+ || Object.keys(payload).length !== 1 || !Object.hasOwn(payload, "actionGrant")) {
187
+ throw knownFailure("workbench_mcp_private_action_grant_missing");
188
+ }
189
+ return privateActionGrant(payload.actionGrant);
190
+ }
191
+ return Object.freeze({
192
+ mode: "mcp-app-rpc",
193
+ actionGrantMode: "host-single-use-v1",
194
+ refreshesWhileHidden,
195
+ readAccountState: () => accountCall("hellolynk_workbench_account", {}, "account"),
196
+ disconnectAccount: (input) => accountCall("hellolynk_workbench_disconnect", input, "disconnection"),
197
+ async readSnapshot(input = {}) {
198
+ return readPrivateSnapshot(Object.fromEntries(
199
+ ["threadId", "threadCursor", "messageCursor", "contactRequestCursor", "query", "filter", "historyBefore"]
200
+ .filter((key) => Object.hasOwn(input, key)
201
+ && input[key] !== ""
202
+ && (key === "contactRequestCursor" || key === "historyBefore" || input[key] != null))
203
+ .map((key) => [key, input[key]]),
204
+ ));
205
+ },
206
+ async connectAccount(input) {
207
+ const response = await client.callTool("hellolynk_workbench_connect", scoped(input), 160_000);
208
+ if (response?.isError) throw knownFailure(response?.structuredContent?.errorCode || "workbench_connect_failed");
209
+ if (!privateOnlyVisibleEnvelope(response)) throw knownFailure("workbench_connect_result_invalid");
210
+ const payload = response?._meta?.hellolynkWorkbench;
211
+ if (!payload || Object.keys(payload).join(",") !== "connection") throw knownFailure("workbench_connect_result_invalid");
212
+ return payload.connection;
213
+ },
214
+ executeCommand: executePrivateCommand,
215
+ subscribe(onChange, onError, onReconnect) {
216
+ let stopped = false;
217
+ let timer;
218
+ let inFlight = false;
219
+ let cursor = 0;
220
+ let generation = null;
221
+ let revision = accountRevision;
222
+ let ready = false;
223
+ let failures = 0;
224
+ let resyncing = false;
225
+ let inViewport = true;
226
+ let cardWasHidden = false;
227
+ let preparedWhileHidden = false;
228
+ let viewportObserver;
229
+ const hidden = () => !refreshesWhileHidden && (pageDocument?.visibilityState === "hidden" || !inViewport);
230
+ const schedule = (delay) => {
231
+ clearTimeout(timer);
232
+ if (!stopped && !hidden() && !inFlight) timer = setTimeout(poll, delay);
233
+ };
234
+ const poll = async () => {
235
+ if (stopped || hidden() || inFlight) return;
236
+ inFlight = true;
237
+ if (revision !== accountRevision) {
238
+ revision = accountRevision; cursor = 0; generation = null; ready = false;
239
+ }
240
+ const observedRevision = revision;
241
+ // A read already in flight when intersection is lost must not treat
242
+ // that event as a sidebar close before visibilitychange can follow.
243
+ const prepareHiddenView = cardWasHidden && !inViewport;
244
+ let resync = false;
245
+ try {
246
+ // The initial account read owns initialization. Never query an
247
+ // unscoped account while that read is still in flight.
248
+ if (observedRevision === null) return;
249
+ const response = await withTimeout(client.callTool("hellolynk_workbench_changes",
250
+ scoped({ afterCursor: cursor }), changeTimeoutMs), changeTimeoutMs, "workbench_mcp_changes_timeout");
251
+ if (stopped || hidden() || observedRevision !== accountRevision) return;
252
+ if (!privateOnlyVisibleEnvelope(response)) throw knownFailure("workbench_mcp_model_visible_changes_rejected");
253
+ if (response?.isError) throw knownFailure(response?._meta?.hellolynkWorkbench?.changeError?.code || "workbench_changes_failed");
254
+ const payload = response?._meta?.hellolynkWorkbench;
255
+ const batch = payload?.changes;
256
+ if (!payload || Object.keys(payload).join(",") !== "changes"
257
+ || !batch || Object.keys(batch).sort().join(",") !== "changed,cursor,hostGeneration,snapshotRequired"
258
+ || !Number.isSafeInteger(batch.cursor) || batch.cursor < 0
259
+ || !/^[a-f0-9]{48}$/.test(batch.hostGeneration)
260
+ || typeof batch.snapshotRequired !== "boolean" || typeof batch.changed !== "boolean") {
261
+ throw knownFailure("workbench_changes_invalid");
262
+ }
263
+ const replaced = generation !== null && generation !== batch.hostGeneration;
264
+ if (!replaced && batch.cursor < cursor) throw knownFailure("host_app_state_conflict");
265
+ cursor = batch.cursor;
266
+ generation = batch.hostGeneration;
267
+ // Prepare a collapsed latest message page while the retained sidebar
268
+ // is away. Check after the async read so a window hide can cancel an
269
+ // apparent card close; minimization must preserve the history window.
270
+ if (refreshesWhileHidden && prepareHiddenView && cardWasHidden && !inViewport && !preparedWhileHidden
271
+ && pageDocument?.visibilityState !== "hidden") {
272
+ preparedWhileHidden = true;
273
+ onReconnect?.({ reason: "background" });
274
+ }
275
+ if (replaced || batch.snapshotRequired || batch.changed) onChange?.({ hostGeneration: generation, changeCursor: String(cursor), snapshotRequired: true });
276
+ if (!ready || failures) onReconnect?.();
277
+ ready = true;
278
+ failures = 0;
279
+ resyncing = false;
280
+ } catch (error) {
281
+ if (stopped || hidden() || observedRevision !== accountRevision) return;
282
+ if (error?.code === "host_app_state_conflict" && !resyncing) {
283
+ // A Host restarted after idle shutdown or an update starts a new
284
+ // cursor and rejects ours. That is a read-cursor reset, not a page
285
+ // failure: an unordered change keeps the view with old writes
286
+ // fenced, and the cursor rereads from zero at once.
287
+ cursor = 0; ready = false; resyncing = resync = true;
288
+ onChange?.({ snapshotRequired: true });
289
+ return;
290
+ }
291
+ resyncing = false;
292
+ if (error?.code === "host_app_state_conflict") cursor = 0;
293
+ failures = Math.min(failures + 1, 5);
294
+ onError?.(error?.code === "host_app_state_conflict"
295
+ ? knownFailure("workbench_change_cursor_resync_failed") : error);
296
+ // Account switches are read-only recovery; permissions and connection
297
+ // establishment remain owned by the existing account UI.
298
+ if (error?.code === "workbench_account_changed") {
299
+ try { await accountCall("hellolynk_workbench_account", {}, "account"); } catch { /* Retry the change source later. */ }
300
+ }
301
+ } finally {
302
+ inFlight = false;
303
+ schedule(resync ? 0 : Math.min(changePollMs * 2 ** failures, 30_000));
304
+ }
305
+ };
306
+ const visibilityChanged = () => {
307
+ // Browsers may deliver intersection loss before visibilitychange when
308
+ // minimizing. A document hide must cancel that apparent card close.
309
+ if (pageDocument?.visibilityState === "hidden") cardWasHidden = false;
310
+ schedule(hidden() ? changePollMs : 0);
311
+ };
312
+ pageDocument?.addEventListener?.("visibilitychange", visibilityChanged);
313
+ // Embedded chat history can remain mounted and report a visible document
314
+ // after its card scrolls away. Observe the frame's viewport intersection
315
+ // as well; hosts without this API retain the bounded document polling.
316
+ if (typeof IntersectionObserver === "function" && pageDocument?.documentElement) {
317
+ try {
318
+ viewportObserver = new IntersectionObserver((entries) => {
319
+ if (stopped) return;
320
+ const entry = entries.find((item) => item.target === pageDocument.documentElement);
321
+ if (!entry || entry.isIntersecting === inViewport) return;
322
+ inViewport = entry.isIntersecting;
323
+ // A sidebar can hide a mounted iframe without hiding its document.
324
+ // Returning to it opens the latest page, even if no data changed.
325
+ if (!inViewport) {
326
+ cardWasHidden = pageDocument?.visibilityState !== "hidden";
327
+ preparedWhileHidden = false;
328
+ }
329
+ else {
330
+ // A minimized/background window can also lose intersection.
331
+ // Only a card hidden while its page was visible counts as closed.
332
+ if (cardWasHidden && pageDocument?.visibilityState !== "hidden") onReconnect?.({ reason: "reopen" });
333
+ cardWasHidden = false;
334
+ }
335
+ // Intersection loss can precede the window's visibility event.
336
+ // Use the normal next probe to prepare a hidden sidebar, allowing
337
+ // minimization to cancel that preparation without another timer.
338
+ if (refreshesWhileHidden && !inViewport) schedule(changePollMs);
339
+ else visibilityChanged();
340
+ });
341
+ viewportObserver.observe(pageDocument.documentElement);
342
+ } catch {
343
+ viewportObserver?.disconnect();
344
+ viewportObserver = null;
345
+ inViewport = true;
346
+ }
347
+ }
348
+ schedule(changePollMs);
349
+ return () => {
350
+ stopped = true; clearTimeout(timer);
351
+ viewportObserver?.disconnect();
352
+ pageDocument?.removeEventListener?.("visibilitychange", visibilityChanged);
353
+ };
354
+ },
355
+ });
356
+ }
357
+
358
+ // Codex removes null-valued object properties from tools/call results, even in
359
+ // private _meta. Restore only explicitly nullable contract fields. Missing
360
+ // containers, required values, and invalid values still fail shared validation.
361
+ function restoreMcpNullFields(value, paths) {
362
+ function restoreAt(node, [key, ...rest]) {
363
+ if (key === "*") return Array.isArray(node) ? node.map(item => restoreAt(item, rest)) : node;
364
+ if (!node || typeof node !== "object" || Array.isArray(node)) return node;
365
+ if (!rest.length) return Object.hasOwn(node, key) ? node : { ...node, [key]: null };
366
+ if (!Object.hasOwn(node, key)) return node;
367
+ const child = restoreAt(node[key], rest);
368
+ return child === node[key] ? node : { ...node, [key]: child };
369
+ }
370
+ return paths.reduce((result, path) => restoreAt(result, path.split(".")), value);
371
+ }
372
+
373
+ function privateOnlyVisibleEnvelope(response) {
374
+ const content = response?.content;
375
+ if (content != null && (!Array.isArray(content) || content.length !== 0)) return false;
376
+ const visible = response?.structuredContent;
377
+ return visible == null || (visible && typeof visible === "object" && !Array.isArray(visible)
378
+ && Object.keys(visible).length === 1 && visible.ok === true);
379
+ }
380
+
381
+ function privateActionGrant(value) {
382
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{31,511}$/.test(value)) {
383
+ throw knownFailure("workbench_mcp_private_action_grant_invalid");
384
+ }
385
+ return value;
386
+ }
387
+
388
+ function knownFailure(code) {
389
+ const error = new Error(code);
390
+ error.code = code;
391
+ error.outcome = "known-failed";
392
+ return error;
393
+ }
394
+
395
+ function knownPreMutationError(code) {
396
+ return /^(?:workbench_(?:mcp_)?action_grant_(?:required|invalid|expired|mismatch|revoked)|host_app_action_grant_(?:required|invalid|expired|mismatch|revoked))$/.test(String(code || ""));
397
+ }
398
+
399
+ function positiveTimeout(value, fallback) {
400
+ if (value == null) return fallback;
401
+ const timeout = Number(value);
402
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("workbench_mcp_snapshot_timeout_invalid");
403
+ return timeout;
404
+ }
405
+
406
+ function withTimeout(value, timeoutMs, code) {
407
+ let timeout;
408
+ const deadline = new Promise((_, reject) => {
409
+ timeout = setTimeout(() => {
410
+ const error = new Error(code);
411
+ error.code = code;
412
+ reject(error);
413
+ }, timeoutMs);
414
+ });
415
+ return Promise.race([Promise.resolve(value), deadline]).finally(() => clearTimeout(timeout));
416
+ }
417
+
418
+ async function startMcpApp() {
419
+ const bridge = createStandardMcpAppClient(window);
420
+ const session = await bridge.connect();
421
+ let resizeEnabled = false;
422
+ const connectedClient = Object.freeze({
423
+ async callTool(name, args, timeoutMs) {
424
+ await bridge.connect();
425
+ if (!resizeEnabled) {
426
+ resizeEnabled = true;
427
+ enableMcpAppSizeNotifications(bridge, session.hostContext);
428
+ }
429
+ return bridge.callTool(name, args, timeoutMs);
430
+ },
431
+ });
432
+ const openPortal = session?.hostCapabilities?.openLinks
433
+ ? async (target) => {
434
+ const url = PORTAL_URLS[target];
435
+ if (!url) throw new Error("workbench_portal_target_invalid");
436
+ const result = await bridge.openLink(url);
437
+ if (result?.isError) throw new Error("workbench_portal_open_failed");
438
+ }
439
+ : null;
440
+ mountWorkbench({ transport: createMcpAppTransport(connectedClient, { hostContext: session.hostContext }), openPortal,
441
+ openConnectionLink: session?.hostCapabilities?.openLinks ? (url) => bridge.openLink(url).then((result) => {
442
+ if (result?.isError) throw new Error("workbench_portal_open_failed");
443
+ }) : undefined,
444
+ });
445
+ }
446
+
447
+ // __HELLOLYNK_MCP_APP_START__
448
+ void startMcpApp().catch((error) => {
449
+ document.getElementById("state-title").textContent = "HelloLynk 暂时打不开 / HelloLynk couldn’t open";
450
+ document.getElementById("state-message").textContent = `页面没能启动。请回到客户端,重新打开 HelloLynk 通讯;一直不行,请把下面的错误码告诉你的 AI。 / The page didn’t start. Go back to your client and reopen HelloLynk Messaging; if it keeps failing, tell your AI the error code below.\n${publicError(error)}`;
451
+ document.querySelector(".spinner")?.setAttribute("hidden", "");
452
+ });