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

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 +101 -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 +277 -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,2748 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import { lstatSync, realpathSync, writeSync } from "node:fs";
5
+ import fs from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+ import { pathToFileURL } from "node:url";
10
+ import { isHelloLynkMcpServerShape, traeConfigPaths } from "./hellolynk-mcp-config-shape.mjs";
11
+
12
+ const PACKAGE_ROOT = path.resolve(import.meta.dirname, "..");
13
+ const MAX_JSON_BYTES = 1024 * 1024;
14
+ const PACK_MANIFEST = "hellolynk-client-pack-manifest.json";
15
+ const PACK_PIN = "hellolynk-client-pack-pin.json";
16
+
17
+ // A person at a terminal reads sentences in the system language; the updater, tests and other
18
+ // programs read one JSON line.
19
+ const jsonOutput = process.argv.includes("--json") || process.stdout.isTTY !== true;
20
+ const CHINESE = /^zh\b/i.test(Intl.DateTimeFormat().resolvedOptions().locale);
21
+ const CLIENT_NAMES = Object.freeze({ codex: "Codex", openclaw: "OpenClaw", "trae-work": "TRAE Work",
22
+ workbuddy: "WorkBuddy", cursor: "Cursor", dsh: "DSH" });
23
+ const { packageJson, metadata } = await readPackage();
24
+ const commandArguments = process.argv.slice(2).filter((value) => value !== "--json");
25
+ const command = String(commandArguments.shift() || "help").toLowerCase();
26
+ const waitForPending = command === "--wait-for-pending";
27
+ const setupArguments = [...commandArguments];
28
+ const migrateCursorPermissions = command === "setup" && metadata.client === "cursor"
29
+ && setupArguments[0] === "--migrate-existing-plugin-permissions";
30
+ if (migrateCursorPermissions) setupArguments.shift();
31
+ const deferReleaseCleanup = command === "setup" && setupArguments.length === 1 && setupArguments[0] === "--defer-release-cleanup";
32
+ let setupRecovery = null;
33
+ let resolvedCodexRunner = null;
34
+ let mutationRecoveryState = "none";
35
+ let candidateClosure = null;
36
+
37
+ try {
38
+ requireNode();
39
+ setupRecovery = command === "setup" ? parseSetupRecoveryOptions(deferReleaseCleanup ? [] : setupArguments) : null;
40
+ const existingDoctor = command === "doctor" ? parseExistingDoctorOptions(commandArguments) : null;
41
+ if (isNativeClient() && !["connect", "setup", "doctor", "help", "--help"].includes(command)) {
42
+ fail("hellolynk_native_command_invalid");
43
+ }
44
+ if (!isNativeClient() && setupRecovery !== null) fail("hellolynk_npm_setup_arguments_invalid");
45
+ if (waitForPending && metadata.client !== "workbuddy") fail("hellolynk_npm_command_invalid");
46
+ if (command !== "connect" && command !== "setup" && command !== "doctor" && commandArguments.length > 0) {
47
+ fail(isNativeClient() ? "hellolynk_native_command_invalid" : "hellolynk_npm_command_invalid");
48
+ }
49
+ // Validate the public command before preparing even the local install root.
50
+ const connectOptions = command === "connect" ? parseConnectOptions(commandArguments) : null;
51
+ const packageClosure = await verifyPackageClosure(PACKAGE_ROOT);
52
+ candidateClosure = packageClosure;
53
+ const sourceReleaseRoot = path.resolve(PACKAGE_ROOT, metadata.releaseRoot);
54
+ const sourceRelease = await verifyRelease(sourceReleaseRoot, PACKAGE_ROOT);
55
+ if (command === "help" || command === "--help") {
56
+ process.stdout.write(helpText());
57
+ } else if (waitForPending) {
58
+ const bootstrap = await importLocal(PACKAGE_ROOT, metadata.entry, "hellolynk_npm_entry_invalid");
59
+ await bootstrap.runWorkBuddyPackageBootstrap([
60
+ "--wait-for-pending", "--root", sourceReleaseRoot,
61
+ "--expected-manifest-sha256", sourceRelease.outerManifestSha256,
62
+ ]);
63
+ } else {
64
+ const originalLocations = resolveLocations();
65
+ const accountModule = await importStableCore(PACKAGE_ROOT, "hellolynk-client-account.mjs", "hellolynk_npm_account_runtime_invalid");
66
+ const baseRoot = accountModule.clientAccountBaseRoot(originalLocations.hostRoot);
67
+ const packageLocations = { ...originalLocations, hostRoot: baseRoot, finalCurrentRoot: path.dirname(baseRoot) };
68
+ const accounts = accountModule.createClientAccountActions({ client: metadata.client, storeRoot: baseRoot });
69
+ if (command === "activate" && !commandArguments.includes("--binding-key-from-stdin")) {
70
+ fail("hellolynk_npm_binding_key_stdin_flag_required");
71
+ }
72
+ const mutating = ["init", "connect", "activate", "setup"].includes(command);
73
+ if (mutating) await prepareFinalClientRoot(packageLocations);
74
+ const revision = connectOptions?.accountRevision
75
+ || setupRecovery?.expectedAccountRevision
76
+ || accounts.readAccountState().revision;
77
+ // Uninstall changes client settings under the account lock too, but never creates the root.
78
+ const run = mutating || (command === "uninstall" && await exists(baseRoot))
79
+ ? (operation) => accounts.runForAccount(revision, operation)
80
+ : (operation) => operation(accounts.assertRevision(revision));
81
+ await run(async (hostRoot) => {
82
+ const locations = { ...packageLocations, hostRoot, finalCurrentRoot: path.dirname(hostRoot), releaseCurrentRoot: packageLocations.finalCurrentRoot };
83
+ let installed = null;
84
+ if (mutating) {
85
+ await prepareFinalClientRoot(locations);
86
+ installed = await installStablePackage({ packageClosure, sourceRelease, locations: packageLocations });
87
+ }
88
+ const activation = await inspectActivation(locations.hostRoot, metadata.profileId);
89
+ // Both public install commands use the actual account state. Native recovery
90
+ // and updater-only options still require an existing activation.
91
+ const initialSetup = !activation.ready && (command === "init"
92
+ || (command === "setup" && !isNativeClient() && !deferReleaseCleanup));
93
+ if (initialSetup) {
94
+ const preparedConfiguration = await preflightClientConfiguration({ ...installed, locations, allowUnconnected: true });
95
+ const host = await initializeHost(locations, installed.packageRoot);
96
+ const configured = await configureClient({ ...installed, locations }, preparedConfiguration);
97
+ const releaseCleanup = await cleanupInstalledReleases(installed);
98
+ print({
99
+ ok: true,
100
+ client: metadata.client,
101
+ hostId: host.hostInstallationId,
102
+ state: host.state,
103
+ configured,
104
+ releaseCleanup,
105
+ accountUrl: `${metadata.serviceBaseUrl}/account`,
106
+ next: "open_client_and_connect",
107
+ installsNode: false,
108
+ installsGit: false,
109
+ installsCodex: false,
110
+ });
111
+ } else if (command === "connect") {
112
+ const preparedConfiguration = await preflightClientConfiguration({ ...installed, locations });
113
+ const host = await initializeHost(locations, installed.packageRoot);
114
+ if (isNativeClient()) await assertNativeEnrollmentProfile({ ...installed, locations, host });
115
+ await preflightBindingActivation({ ...installed, locations, activation, preBind: true });
116
+ const relay = await importStableCore(
117
+ installed.packageRoot,
118
+ "hellolynk-relay-client-cli.mjs",
119
+ "hellolynk_npm_activation_runtime_invalid",
120
+ );
121
+ mutationRecoveryState = "device_connect_attempted";
122
+ // The protected enrollment store owns the request and registration intent.
123
+ // A subsequent explicit connect resumes them; this wrapper never retries HTTP.
124
+ const connected = await relay.runRelayClientCli([
125
+ "connect",
126
+ "--hellolynk-id", connectOptions.hellolynkId,
127
+ "--base-url", metadata.serviceBaseUrl,
128
+ "--root", locations.finalCurrentRoot,
129
+ "--neutral-host-root", locations.hostRoot,
130
+ "--adapter", metadata.client,
131
+ "--profile", metadata.profileId,
132
+ ...(connectOptions.instanceName ? ["--instance-name", connectOptions.instanceName] : []),
133
+ ...(connectOptions.deviceLabel ? ["--device-label", connectOptions.deviceLabel] : []),
134
+ ]);
135
+ if (connected?.ok !== true || connected.hellolynkId !== connectOptions.hellolynkId) {
136
+ fail("hellolynk_npm_connect_response_invalid");
137
+ }
138
+ if (connected.status === "confirmation_required") {
139
+ const verificationUrl = checkedVerificationUrl(connected.verificationUrl);
140
+ mutationRecoveryState = "none";
141
+ print({
142
+ ok: true,
143
+ status: "confirmation_required",
144
+ client: metadata.client,
145
+ hellolynkId: connectOptions.hellolynkId,
146
+ verificationUrl,
147
+ activated: false,
148
+ instructions: {
149
+ zh: "还差一步:请打开下方官网链接,确认这台电脑。确认后,让 AI 继续连接;同一台电脑只需确认一次。",
150
+ en: "One more step: open the website link below and confirm this computer. Then ask your AI to continue connecting. Each computer only needs to be confirmed once.",
151
+ },
152
+ next: "confirm_on_website_then_repeat_connect",
153
+ });
154
+ } else {
155
+ if (connected.status !== "registered" || connected.clientType !== metadata.client
156
+ || connected.profileId !== metadata.profileId || connected.hostProfileRegistered !== true
157
+ || connected.configPath !== path.join(locations.finalCurrentRoot, "profiles", `${metadata.profileId}.json`)) {
158
+ fail("hellolynk_npm_connect_response_invalid");
159
+ }
160
+ const instanceName = checkedConnectLabel(connected.instanceName);
161
+ mutationRecoveryState = "device_registration_committed_roll_forward_required";
162
+ const activated = await inspectActivation(locations.hostRoot, metadata.profileId);
163
+ if (!activated.ready) fail("hellolynk_npm_host_activation_failed");
164
+ await activateBinding({ ...installed, locations, activation: activated });
165
+ const configured = await configureClient({ ...installed, locations }, preparedConfiguration);
166
+ mutationRecoveryState = "none";
167
+ const releaseCleanup = await cleanupInstalledReleases(installed);
168
+ const hostHandover = await requestInstalledHostHandover({ ...installed, locations, activation: activated });
169
+ print({
170
+ ok: true,
171
+ status: "registered",
172
+ client: metadata.client,
173
+ hellolynkId: connectOptions.hellolynkId,
174
+ instanceName,
175
+ ...(connected.recoveredExistingName === true ? { recoveredExistingName: true } : {}),
176
+ profileId: metadata.profileId,
177
+ hostId: host.hostInstallationId,
178
+ activated: true,
179
+ configured,
180
+ releaseCleanup,
181
+ hostHandover,
182
+ restartRequired: true,
183
+ ...(isNativeClient() ? { next: "reopen_native_workbench" } : {}),
184
+ installsNode: false,
185
+ installsGit: false,
186
+ installsCodex: false,
187
+ });
188
+ }
189
+ } else if (command === "activate") {
190
+ const preparedConfiguration = await preflightClientConfiguration({ ...installed, locations });
191
+ const host = await initializeHost(locations, installed.packageRoot);
192
+ await preflightBindingActivation({ ...installed, locations, activation, preBind: true });
193
+ const relay = await importStableCore(
194
+ installed.packageRoot,
195
+ "hellolynk-relay-client-cli.mjs",
196
+ "hellolynk_npm_activation_runtime_invalid",
197
+ );
198
+ const bindArgs = [
199
+ "bind",
200
+ "--binding-key-from-stdin",
201
+ "--base-url", metadata.serviceBaseUrl,
202
+ "--device-id", `hellolynk_${metadata.client.replaceAll("-", "_")}_${host.hostInstallationId.slice(-12)}`,
203
+ "--host-installation-id", host.hostInstallationId,
204
+ "--profile", metadata.profileId,
205
+ "--root", locations.finalCurrentRoot,
206
+ "--neutral-host-root", locations.hostRoot,
207
+ "--adapter", metadata.client,
208
+ ];
209
+ const needsBootstrapTransport = host.state === "unclaimed"
210
+ || !(await hasHostTransport(locations, installed.packageRoot));
211
+ if (needsBootstrapTransport) bindArgs.splice(2, 0, "--bootstrap-host-transport");
212
+ mutationRecoveryState = "binding_redemption_attempted";
213
+ const bound = await relay.runRelayClientCli(bindArgs);
214
+ if (bound?.neutralHostActivated !== true) fail("hellolynk_npm_host_activation_failed");
215
+ mutationRecoveryState = "binding_committed_roll_forward_required";
216
+ const activated = await inspectActivation(locations.hostRoot, metadata.profileId);
217
+ if (!activated.ready) fail("hellolynk_npm_host_activation_failed");
218
+ await activateBinding({ ...installed, locations, activation: activated });
219
+ const configured = await configureClient({ ...installed, locations }, preparedConfiguration);
220
+ mutationRecoveryState = "none";
221
+ const releaseCleanup = await cleanupInstalledReleases(installed);
222
+ const hostHandover = await requestInstalledHostHandover({ ...installed, locations, activation: activated });
223
+ print({
224
+ ok: true,
225
+ client: metadata.client,
226
+ hostId: host.hostInstallationId,
227
+ activated: true,
228
+ bootstrapTransportStored: bound.bootstrapTransportStored === true,
229
+ configured,
230
+ releaseCleanup,
231
+ hostHandover,
232
+ restartRequired: true,
233
+ installsNode: false,
234
+ installsGit: false,
235
+ installsCodex: false,
236
+ });
237
+ } else if (command === "doctor") {
238
+ if (existingDoctor && !activation.ready) fail("hellolynk_npm_existing_host_activation_required");
239
+ const installed = existingDoctor
240
+ ? await inspectExistingDoctorTarget(existingDoctor, { packageClosure, locations, packageLocations, activation })
241
+ : activation.ready ? await inspectStablePackage({ packageClosure, sourceRelease, locations: packageLocations }) : null;
242
+ if (installed) {
243
+ await verifyExactBinding({ ...installed, locations, activation });
244
+ await verifyClientConfiguration({ ...installed, locations });
245
+ }
246
+ print({
247
+ ok: true,
248
+ client: metadata.client,
249
+ packageVersion: existingDoctor ? installed.packageJson.version : packageJson.version,
250
+ ...(existingDoctor ? { existingPackageManifestSha256: installed.packageClosure.manifestSha256,
251
+ compatibilityReaderVersion: packageJson.version, readOnly: true } : {}),
252
+ releaseVerified: true,
253
+ activated: activation.ready,
254
+ activationState: activation.state,
255
+ stableReleaseVerified: installed != null,
256
+ exactBindingVerified: installed != null,
257
+ clientConfigurationVerified: installed != null,
258
+ installsNode: false,
259
+ installsGit: false,
260
+ installsCodex: false,
261
+ });
262
+ } else if (command === "setup" || command === "init") {
263
+ if (!activation.ready) fail("hellolynk_npm_existing_host_activation_required");
264
+ if (setupRecovery !== null) {
265
+ if (!isNativeClient() || installed.release.manifestSha256 !== setupRecovery.expectedCurrentManifestSha256) {
266
+ fail("hellolynk_native_recovery_candidate_mismatch");
267
+ }
268
+ const currentBinding = await verifyExactBinding({ ...installed, locations, activation });
269
+ const previous = await inspectTrustedPackageRoot({
270
+ packageRoot: setupRecovery.previousPackageRoot,
271
+ currentPackageRoot: installed.packageRoot,
272
+ locations,
273
+ activation,
274
+ });
275
+ if (samePath(previous.packageRoot, installed.packageRoot)) fail("hellolynk_native_recovery_previous_invalid");
276
+ const previousConfiguration = metadata.client === "cursor"
277
+ ? await preflightClientConfiguration({ ...previous, previousCursorPackage: installed }) : null;
278
+ if (previousConfiguration) {
279
+ try {
280
+ await configureClient(previous, previousConfiguration, { bindingContext: { ...installed, locations, activation } });
281
+ await verifyClientConfiguration(previous);
282
+ } catch (error) {
283
+ // Keep the candidate binding until the complete old configuration
284
+ // verifies. An old doctor cannot see the new plugin, so switching
285
+ // first could misreport a partial rollback as a successful one.
286
+ try { await establishClientState({ ...installed, locations, activation }, previousConfiguration, previous); }
287
+ catch { throw codexUpgradeOutcomeUncertain(error); }
288
+ throw error;
289
+ }
290
+ }
291
+ mutationRecoveryState = "protected_binding_update_attempted";
292
+ await activateBinding({ ...previous, expectedOwner: bindingOwner(currentBinding) });
293
+ mutationRecoveryState = "binding_committed_roll_forward_required";
294
+ await verifyExactBinding(previous);
295
+ if (previousConfiguration) await verifyClientConfiguration(previous);
296
+ mutationRecoveryState = "none";
297
+ const hostHandover = await requestInstalledHostHandover(previous, { restore: true });
298
+ print({
299
+ ok: true,
300
+ client: metadata.client,
301
+ packageVersion: packageJson.version,
302
+ recovered: true,
303
+ restoredPackageVersion: previous.packageJson.version,
304
+ activated: true,
305
+ hostHandover,
306
+ restartRequired: true,
307
+ next: "reopen_native_workbench",
308
+ installsNode: false,
309
+ installsGit: false,
310
+ installsCodex: false,
311
+ });
312
+ return;
313
+ }
314
+ const current = { ...installed, locations, activation };
315
+ // Authenticate the old package and its unchanged Skill before proposing a
316
+ // Cursor upgrade. Doctor remains strict and never accepts a previous Skill.
317
+ const cursorPrevious = metadata.client === "cursor" ? await inspectPreviousOrAbsent(current) : null;
318
+ const preparedConfiguration = await preflightClientConfiguration({ ...current, previousCursorPackage: cursorPrevious });
319
+ await preflightBindingActivation(current);
320
+ let configured, previousPackageRoot = null;
321
+ if (metadata.client === "codex" && preparedConfiguration?.upgrade === true) {
322
+ previousPackageRoot = preparedConfiguration.previous.packageRoot;
323
+ configured = await upgradeCodexConfigurationAndBinding(
324
+ { ...installed, locations, activation },
325
+ preparedConfiguration,
326
+ );
327
+ } else {
328
+ const previous = metadata.client === "cursor" ? cursorPrevious : await inspectPreviousOrAbsent(current);
329
+ if (previous && !samePath(previous.packageRoot, current.packageRoot)) {
330
+ previousPackageRoot = previous.packageRoot;
331
+ configured = await upgradeClientConfigurationAndBinding(current, previous, preparedConfiguration);
332
+ } else {
333
+ mutationRecoveryState = "protected_binding_update_attempted";
334
+ await activateBinding(current);
335
+ mutationRecoveryState = "binding_committed_roll_forward_required";
336
+ configured = await configureClient(current, preparedConfiguration);
337
+ }
338
+ }
339
+ mutationRecoveryState = "none";
340
+ const releaseCleanup = deferReleaseCleanup ? { deferredToUpdater: true }
341
+ : await cleanupInstalledReleases(installed, previousPackageRoot);
342
+ const hostHandover = await requestInstalledHostHandover(current);
343
+ print({
344
+ ok: true,
345
+ client: metadata.client,
346
+ packageVersion: packageJson.version,
347
+ activated: true,
348
+ configured,
349
+ releaseCleanup,
350
+ hostHandover,
351
+ restartRequired: true,
352
+ ...(isNativeClient() ? { next: "reopen_native_workbench" } : {}),
353
+ installsNode: false,
354
+ installsGit: false,
355
+ installsCodex: false,
356
+ });
357
+ } else if (command === "uninstall") {
358
+ const uninstalled = await uninstallClient(locations);
359
+ print({ ok: true, client: metadata.client, uninstalled: true, ...uninstalled, restartRequired: true });
360
+ } else {
361
+ fail("hellolynk_npm_command_invalid");
362
+ }
363
+ });
364
+ }
365
+ } catch (error) {
366
+ const failure = failureResult(error, mutationRecoveryState);
367
+ process.stderr.write(jsonOutput ? `${JSON.stringify(failure)}\n` : failureText(failure));
368
+ process.exitCode = 1;
369
+ }
370
+
371
+ async function cleanupInstalledReleases(installed, previousRelease = null) {
372
+ try {
373
+ const cleanup = await importStableCore(installed.packageRoot, "hellolynk-release-retention.mjs", "hellolynk_release_retention_unavailable");
374
+ return await cleanup.pruneInstalledReleases({ releasesRoot: path.dirname(installed.packageRoot),
375
+ client: metadata.client, currentRelease: installed.packageRoot, previousRelease });
376
+ } catch { return { complete: false, error: "release_retention_unavailable" }; }
377
+ }
378
+
379
+ // Uninstall reverses what setup wrote for this client, withdraws its local authorization and
380
+ // removes its releases; one still running waits for the next install or uninstall. host\ keeps
381
+ // the account, this computer's connection, contacts and history, so installing again issues a
382
+ // new authorization without connecting again.
383
+ async function uninstallClient(locations) {
384
+ const releasesRoot = path.join(locations.releaseCurrentRoot, "releases");
385
+ const releases = await installedReleaseRoots(releasesRoot);
386
+ const result = {};
387
+ if (metadata.client === "codex") {
388
+ result.codexPluginRemoved = await removeCodexMarketplace(locations, releasesRoot);
389
+ } else {
390
+ const module = await importLocal(PACKAGE_ROOT, metadata.configurator, "hellolynk_npm_configurator_invalid");
391
+ const remove = metadata.client === "openclaw" ? module.removeOpenClaw : module.removeNeutralHostMcpClient;
392
+ const plans = [];
393
+ for (const configPath of uninstallConfigPaths(locations)) {
394
+ const options = { client: metadata.client, configPath, releasesRoot };
395
+ const plan = await remove({ ...options, apply: false })
396
+ .catch(uninstallConflict("agent_setup_uninstall_conflict", "hellolynk_npm_uninstall_config_conflict"));
397
+ if (plan.changed) plans.push({ remove, options, plan });
398
+ }
399
+ // TRAE's argv.json stays as it is: setup may have kept a debugging port the user set, and
400
+ // nothing records which. Only the receiver's own setting goes.
401
+ const receiver = metadata.client === "trae-work"
402
+ ? await importLocal(PACKAGE_ROOT, "integrations/trae-work/server/native-task-worker.mjs", "hellolynk_npm_trae_receiver_invalid") : null;
403
+ const receiverConfigured = receiver && await exists(locations.hostRoot)
404
+ && await receiver.readTraeNativeTaskWorkerConfig(locations.hostRoot);
405
+ const skill = metadata.skillTarget
406
+ ? await importLocal(PACKAGE_ROOT, "tools/hellolynk-install-client-skills.mjs", "hellolynk_npm_skill_installer_invalid") : null;
407
+ const skillOptions = { sourceRoots: [PACKAGE_ROOT, ...releases], home: locations.home, appData: locations.appData,
408
+ localAppData: locations.localAppData, target: metadata.skillTarget };
409
+ const skillPlan = skill && await skill.removeInstalledClientSkill(skillOptions)
410
+ .catch(uninstallConflict("client_skill_destination_differs", "hellolynk_npm_uninstall_skill_conflict"));
411
+ // WorkBuddy's receiver and page tasks run from the releases removed below.
412
+ if (metadata.client === "workbuddy") result.services = await uninstallWorkBuddyServices(locations, releases);
413
+ const transaction = await module.runAgentConfigurationTransaction({
414
+ plans,
415
+ apply: ({ remove, options, plan }) => remove({ ...options, expectedConfigPath: plan.configPath,
416
+ expectedConfigSha256: plan.expectedConfigSha256, expectedReplacementSha256: plan.replacementSha256 }),
417
+ rollback: (item) => module.rollbackAgentConfiguration(item),
418
+ finalize: skillPlan?.present ? () => skill.removeInstalledClientSkill({ ...skillOptions, apply: true }) : null,
419
+ });
420
+ result.configPaths = transaction.committed.map((item) => item.configPath);
421
+ if (skillPlan) result.skillRemoved = skillPlan.present;
422
+ if (receiverConfigured) await receiver.configureTraeNativeTaskWorker({ storeRoot: locations.hostRoot, enabled: false });
423
+ }
424
+ // A binding names the release it starts, so it cannot outlive the releases removed below.
425
+ if (await exists(path.join(locations.hostRoot, "capabilities-v1.dpapi.clixml"))) {
426
+ const store = await importStableCore(PACKAGE_ROOT, "hellolynk-host-protected-store.mjs", "hellolynk_npm_binding_invalid");
427
+ await store.createHostProtectedStore({ root: locations.hostRoot, followClientAccount: false }).disconnectLocalClient(metadata.client);
428
+ }
429
+ const retention = await importStableCore(PACKAGE_ROOT, "hellolynk-release-retention.mjs", "hellolynk_release_retention_unavailable");
430
+ result.releaseCleanup = await retention.removeInstalledReleases({ releasesRoot, client: metadata.client });
431
+ return result;
432
+ }
433
+
434
+ function uninstallConflict(code, replacement) {
435
+ return (error) => { if (String(error?.message || "") === code) fail(replacement); throw error; };
436
+ }
437
+
438
+ async function installedReleaseRoots(releasesRoot) {
439
+ const names = await fs.readdir(releasesRoot).catch((error) => { if (error.code === "ENOENT") return []; throw error; });
440
+ return names.filter((name) => new RegExp(`^${metadata.client}-[a-f0-9]{64}$`).test(name)).map((name) => path.join(releasesRoot, name));
441
+ }
442
+
443
+ // Setup writes one of these; uninstall checks every place an earlier setup could have used.
444
+ function uninstallConfigPaths({ home, appData }) {
445
+ if (metadata.client === "openclaw") return [path.join(home, ".openclaw", "openclaw.json")];
446
+ if (metadata.client === "trae-work") return ["Trae CN", "TRAE SOLO CN"].map((name) => path.join(appData, name, "User", "mcp.json"));
447
+ return [".mcp.json", "mcp.json"].map((name) => path.join(home, ".workbuddy", name));
448
+ }
449
+
450
+ async function removeCodexMarketplace(locations, releasesRoot) {
451
+ const marketplace = "hellolynk-npm-codex";
452
+ // Without this marketplace in Codex's settings there is nothing to remove, and no Codex is needed.
453
+ const settings = await fs.readFile(path.join(locations.codexHome, "config.toml"), "utf8").catch(() => "");
454
+ if (!settings.includes(marketplace)) return false;
455
+ const root = codexMarketplaceRoot(locations, marketplace);
456
+ if (root === null) return false;
457
+ if (!samePath(path.dirname(root), releasesRoot) || !/^codex-[a-f0-9]{64}$/i.test(path.basename(root))) {
458
+ fail("hellolynk_npm_uninstall_config_conflict");
459
+ }
460
+ tryRunCodex(["plugin", "remove", `hellolynk-codex@${marketplace}`, "--json"], locations.codexHome);
461
+ runCodex(["plugin", "marketplace", "remove", marketplace, "--json"],
462
+ "hellolynk_npm_codex_marketplace_remove_failed", locations.codexHome);
463
+ if (codexMarketplaceRoot(locations, marketplace) !== null) fail("hellolynk_npm_codex_marketplace_remove_failed");
464
+ return true;
465
+ }
466
+
467
+ async function uninstallWorkBuddyServices(locations, releases) {
468
+ const code = "hellolynk_npm_workbuddy_page_service_invalid";
469
+ const tasks = await importLocal(PACKAGE_ROOT, "integrations/workbuddy/server/workbench-process.mjs", code);
470
+ const registration = await importLocal(PACKAGE_ROOT, "integrations/workbuddy/server/native-task-service-registration.mjs", code);
471
+ const record = await registration.withWorkBuddyNativeServiceRecord(locations.hostRoot,
472
+ async (registry) => (registry ? registry.read() : null));
473
+ if (record) await tasks.configureWorkBuddyNativeTaskService({ ...record, storeRoot: locations.hostRoot }, "Uninstall");
474
+ for (const root of releases) {
475
+ const release = await verifyRelease(root, root).catch(() => null);
476
+ if (release) {
477
+ await tasks.configureWorkBuddyWorkbenchTask({ root, expectedManifestSha256: release.outerManifestSha256,
478
+ storeRoot: locations.hostRoot }, "Uninstall");
479
+ }
480
+ }
481
+ return { nativeServiceRemoved: record !== null };
482
+ }
483
+
484
+ function parseConnectOptions(args) {
485
+ const options = Object.create(null);
486
+ const names = new Map([
487
+ ["--hellolynk-id", "hellolynkId"],
488
+ ["--instance-name", "instanceName"],
489
+ ["--device-label", "deviceLabel"],
490
+ ["--account-revision", "accountRevision"],
491
+ ]);
492
+ for (let index = 0; index < args.length; index += 2) {
493
+ const name = names.get(args[index]);
494
+ const value = args[index + 1];
495
+ if (!name || Object.hasOwn(options, name) || typeof value !== "string"
496
+ || value.startsWith("--")) fail("hellolynk_npm_connect_arguments_invalid");
497
+ options[name] = value;
498
+ }
499
+ if (!/^hellolynk:(?:intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/.test(options.hellolynkId || "")) {
500
+ fail("hellolynk_npm_connect_id_invalid");
501
+ }
502
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$/.test(metadata.profileId)) fail("hellolynk_npm_package_invalid");
503
+ if (Object.hasOwn(options, "instanceName")) {
504
+ options.instanceName = checkedRequestedInstanceName(options.instanceName);
505
+ }
506
+ if (Object.hasOwn(options, "deviceLabel")) options.deviceLabel = checkedConnectLabel(options.deviceLabel);
507
+ if (options.accountRevision !== undefined && !/^(?:initial|[a-f0-9]{32})$/.test(options.accountRevision)) {
508
+ fail("workbench_account_changed");
509
+ }
510
+ return Object.freeze(options);
511
+ }
512
+
513
+ function parseExistingDoctorOptions(args) {
514
+ if (args.length === 0) return null;
515
+ const code = "hellolynk_npm_doctor_arguments_invalid";
516
+ if (!["codex", "dsh"].includes(metadata.client) || args.length !== 4) fail(code);
517
+ const values = Object.create(null);
518
+ for (let index = 0; index < args.length; index += 2) {
519
+ const name = args[index], value = args[index + 1];
520
+ if (!["--existing-root", "--expected-existing-manifest-sha256"].includes(name)
521
+ || Object.hasOwn(values, name) || typeof value !== "string" || !value) fail(code);
522
+ values[name] = value;
523
+ }
524
+ const packageRoot = values["--existing-root"], manifestSha256 = values["--expected-existing-manifest-sha256"];
525
+ if (!path.isAbsolute(packageRoot || "") || path.resolve(packageRoot) !== packageRoot
526
+ || !/^[a-f0-9]{64}$/u.test(manifestSha256 || "")) fail(code);
527
+ return Object.freeze({ packageRoot, manifestSha256 });
528
+ }
529
+
530
+ async function inspectExistingDoctorTarget(target, { packageClosure, locations, packageLocations, activation }) {
531
+ const releasesRoot = path.join(packageLocations.finalCurrentRoot, "releases");
532
+ if (!samePath(target.packageRoot, path.join(releasesRoot, `${metadata.client}-${target.manifestSha256}`))) {
533
+ fail("hellolynk_npm_previous_install_invalid");
534
+ }
535
+ const input = { packageRoot: target.packageRoot,
536
+ currentPackageRoot: path.join(releasesRoot, `${metadata.client}-${packageClosure.manifestSha256}`), locations, activation };
537
+ const previous = metadata.client === "codex"
538
+ ? await inspectTrustedCodexInstallation(input) : await inspectTrustedPackageRoot(input);
539
+ if (previous.packageClosure.manifestSha256 !== target.manifestSha256) fail("hellolynk_npm_previous_install_invalid");
540
+ return previous;
541
+ }
542
+
543
+ function parseSetupRecoveryOptions(args) {
544
+ if (args.length === 0) return null;
545
+ if (args.length !== 6) fail("hellolynk_npm_setup_arguments_invalid");
546
+ const values = Object.create(null);
547
+ for (let index = 0; index < args.length; index += 2) {
548
+ const name = args[index];
549
+ const value = args[index + 1];
550
+ if (!new Set([
551
+ "--recover-previous-root",
552
+ "--expected-current-release-manifest-sha256",
553
+ "--expected-account-revision",
554
+ ]).has(name) || Object.hasOwn(values, name) || typeof value !== "string" || !value) {
555
+ fail("hellolynk_npm_setup_arguments_invalid");
556
+ }
557
+ values[name] = value;
558
+ }
559
+ const previousPackageRoot = values["--recover-previous-root"];
560
+ const expectedCurrentManifestSha256 = values["--expected-current-release-manifest-sha256"];
561
+ const expectedAccountRevision = values["--expected-account-revision"];
562
+ if (!path.isAbsolute(previousPackageRoot) || path.resolve(previousPackageRoot) !== previousPackageRoot
563
+ || !/^[a-f0-9]{64}$/u.test(expectedCurrentManifestSha256 || "")
564
+ || !/^(?:initial|[a-f0-9]{32})$/u.test(expectedAccountRevision || "")) {
565
+ fail("hellolynk_npm_setup_arguments_invalid");
566
+ }
567
+ return Object.freeze({ previousPackageRoot, expectedCurrentManifestSha256, expectedAccountRevision });
568
+ }
569
+
570
+ function checkedConnectLabel(value) {
571
+ if (typeof value !== "string" || !value.trim() || [...value.trim()].length > 80
572
+ || /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(value)) {
573
+ fail("hellolynk_npm_connect_label_invalid");
574
+ }
575
+ return value.trim();
576
+ }
577
+
578
+ function checkedRequestedInstanceName(value) {
579
+ if (typeof value !== "string" || !value || [...value].length > 80
580
+ || /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(value)) {
581
+ fail("hellolynk_npm_connect_label_invalid");
582
+ }
583
+ // The protected enrollment store decides whether this exact value resumes a
584
+ // legacy intent or must satisfy the stricter rule for a new registration.
585
+ return value;
586
+ }
587
+
588
+ function checkedVerificationUrl(value) {
589
+ try {
590
+ const url = new URL(value);
591
+ if (url.origin !== metadata.serviceBaseUrl || url.username || url.password || url.hash
592
+ || url.pathname !== "/account" || url.searchParams.size !== 1
593
+ || !/^devreq_[a-f0-9]{48}$/.test(url.searchParams.get("deviceRequest") || "")) {
594
+ fail("hellolynk_npm_connect_response_invalid");
595
+ }
596
+ return url.href;
597
+ } catch { fail("hellolynk_npm_connect_response_invalid"); }
598
+ }
599
+
600
+ // Until the package names its client, a failure is reported without one.
601
+ async function readPackage() {
602
+ try {
603
+ const packageJson = await readJson(path.join(PACKAGE_ROOT, "package.json"), "hellolynk_npm_package_invalid");
604
+ return { packageJson, metadata: normalizeMetadata(packageJson.hellolynkClient, packageJson) };
605
+ } catch (error) {
606
+ const code = safeCode(error);
607
+ writeSync(2, jsonOutput ? `${JSON.stringify({ ok: false, error: code })}\n` : `${code}\n${failureReason(code, "HelloLynk")}\n`);
608
+ process.exit(1);
609
+ }
610
+ }
611
+
612
+ function normalizeMetadata(value, packageJson) {
613
+ const allowed = [
614
+ "activationTool", "client", "configurator", "coreRoot", "entry", "outerManifestPin",
615
+ "pluginPath", "profileId", "releaseRoot", "serviceBaseUrl", "skillTarget",
616
+ ];
617
+ if (!value || typeof value !== "object" || Array.isArray(value)
618
+ || Object.keys(value).some((key) => !allowed.includes(key))) fail("hellolynk_npm_package_invalid");
619
+ const client = String(value.client || "");
620
+ if (!["codex", "openclaw", "trae-work", "workbuddy", "cursor", "dsh"].includes(client)) fail("hellolynk_npm_package_invalid");
621
+ if (["cursor", "dsh"].includes(client) && (packageJson.private !== true
622
+ || Object.keys(value).sort().join(",") !== "activationTool,client,coreRoot,profileId,releaseRoot,serviceBaseUrl"
623
+ || value.profileId !== `hellolynk-host-${client}-v2`
624
+ || value.serviceBaseUrl !== "https://hellolynk.com" || value.releaseRoot !== "."
625
+ || value.coreRoot !== "runtime/core"
626
+ || value.activationTool !== "tools/hellolynk-activate-neutral-host-clients.mjs")) {
627
+ fail("hellolynk_native_package_invalid");
628
+ }
629
+ const required = ["activationTool", "coreRoot", "profileId", "releaseRoot"];
630
+ if (required.some((key) => !safeRelative(value[key]))) fail("hellolynk_npm_package_invalid");
631
+ for (const key of ["configurator", "entry", "outerManifestPin", "pluginPath", "skillTarget"]) {
632
+ if (value[key] != null && !safeRelative(value[key])) fail("hellolynk_npm_package_invalid");
633
+ }
634
+ let serviceBaseUrl;
635
+ try {
636
+ const parsed = new URL(String(value.serviceBaseUrl || ""));
637
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password
638
+ || parsed.pathname !== "/" || parsed.search || parsed.hash) fail("hellolynk_npm_package_invalid");
639
+ serviceBaseUrl = parsed.origin;
640
+ } catch (error) {
641
+ if (error?.code) throw error;
642
+ fail("hellolynk_npm_package_invalid");
643
+ }
644
+ return Object.freeze({ ...value, client, serviceBaseUrl });
645
+ }
646
+
647
+ function isNativeClient() { return metadata.client === "cursor" || metadata.client === "dsh"; }
648
+
649
+ async function assertNativeEnrollmentProfile({ packageRoot, locations, host }) {
650
+ // The native package has one fixed profile. An older binding or unfinished
651
+ // registration for this client is not permission to create a second identity.
652
+ if (host.delegateBindings.some((binding) => (
653
+ binding.adapters.includes(metadata.client) && binding.profileId !== metadata.profileId
654
+ ) || (binding.profileId === metadata.profileId
655
+ && (binding.adapters.length !== 1 || binding.adapters[0] !== metadata.client)))) {
656
+ fail("hellolynk_native_existing_profile_conflict");
657
+ }
658
+ const protectedModule = await importStableCore(packageRoot,
659
+ "hellolynk-host-protected-store.mjs", "hellolynk_npm_binding_invalid");
660
+ const enrollment = await protectedModule.createHostProtectedStore({ root: locations.hostRoot }).loadDeviceEnrollment();
661
+ if (enrollment.value === null) return;
662
+ const intents = enrollment.value.intents;
663
+ if (!intents || typeof intents !== "object" || Array.isArray(intents)) fail("device_enrollment_store_invalid");
664
+ for (const [profileId, intent] of Object.entries(intents)) {
665
+ if ((intent?.clientType === metadata.client && profileId !== metadata.profileId)
666
+ || (profileId === metadata.profileId && intent?.clientType !== metadata.client)) {
667
+ fail("hellolynk_native_existing_profile_conflict");
668
+ }
669
+ }
670
+ }
671
+
672
+ function safeRelative(value) {
673
+ const text = String(value || "");
674
+ return !!text && text.length <= 512 && !path.isAbsolute(text)
675
+ && !text.split(/[\\/]/).includes("..") && !/[\0\r\n]/.test(text);
676
+ }
677
+
678
+ function requireNode() {
679
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(process.versions.node || ""));
680
+ if (!match || Number(match[1]) < 22 || (Number(match[1]) === 22 && Number(match[2]) < 16)) {
681
+ fail("hellolynk_node_22_16_required");
682
+ }
683
+ }
684
+
685
+ async function verifyPackageClosure(root, expectedManifestSha256 = null, expected = {}) {
686
+ const expectedPackageJson = expected.packageJson || packageJson;
687
+ const expectedMetadata = expected.metadata || metadata;
688
+ const manifestPath = path.join(root, PACK_MANIFEST);
689
+ const pin = await readJson(path.join(root, PACK_PIN), "hellolynk_npm_pack_invalid", true);
690
+ const manifestBytes = await readRegularFile(manifestPath, "hellolynk_npm_pack_invalid", true);
691
+ const manifestSha256 = sha256(manifestBytes);
692
+ if (manifestBytes.length > MAX_JSON_BYTES
693
+ || pin?.contract !== "hellolynk-client-npm-pack-pin-v1"
694
+ || Object.keys(pin).sort().join(",") !== "contract,manifestSha256"
695
+ || pin.manifestSha256 !== manifestSha256
696
+ || (expectedManifestSha256 != null && expectedManifestSha256 !== manifestSha256)) {
697
+ fail("hellolynk_npm_pack_invalid");
698
+ }
699
+ let manifest;
700
+ try { manifest = JSON.parse(manifestBytes.toString("utf8")); }
701
+ catch { fail("hellolynk_npm_pack_invalid"); }
702
+ if (manifest?.contract !== "hellolynk-client-npm-pack-manifest-v1"
703
+ || Object.keys(manifest).sort().join(",") !== "client,contract,files,packageName,version,workbench"
704
+ || manifest.client !== expectedMetadata.client || manifest.packageName !== expectedPackageJson.name
705
+ || manifest.version !== expectedPackageJson.version || !Array.isArray(manifest.files)
706
+ || manifest.files.length < 8 || manifest.files.length > 5_000) fail("hellolynk_npm_pack_invalid");
707
+
708
+ const rootRealPath = await fs.realpath(root).catch(() => fail("hellolynk_npm_pack_invalid"));
709
+ const records = new Map();
710
+ const portablePathNodes = new Map();
711
+ registerPortablePackPath(portablePathNodes, PACK_MANIFEST);
712
+ registerPortablePackPath(portablePathNodes, PACK_PIN);
713
+ for (const record of manifest.files) {
714
+ if (!record || typeof record !== "object" || Array.isArray(record)
715
+ || Object.keys(record).sort().join(",") !== "bytes,path,sha256"
716
+ || !safePackPath(record.path) || !Number.isSafeInteger(record.bytes) || record.bytes < 0
717
+ || !/^[a-f0-9]{64}$/.test(record.sha256) || records.has(record.path)) fail("hellolynk_npm_pack_invalid");
718
+ registerPortablePackPath(portablePathNodes, record.path);
719
+ const file = path.resolve(root, ...record.path.split("/"));
720
+ const fileRealPath = await fs.realpath(file).catch(() => fail("hellolynk_npm_pack_invalid"));
721
+ const relativeRealPath = path.relative(rootRealPath, fileRealPath);
722
+ if (!relativeRealPath || path.isAbsolute(relativeRealPath)
723
+ || relativeRealPath === ".." || relativeRealPath.startsWith(`..${path.sep}`)) fail("hellolynk_npm_pack_invalid");
724
+ const body = await readRegularFile(file, "hellolynk_npm_pack_invalid", true);
725
+ if (body.length !== record.bytes || sha256(body) !== record.sha256) fail("hellolynk_npm_pack_invalid");
726
+ records.set(record.path, record);
727
+ }
728
+ await assertExactPackageTree(root, records, rootRealPath);
729
+ if (!records.has("package.json") || !records.has("bin/hellolynk-client-setup.mjs")) fail("hellolynk_npm_pack_invalid");
730
+ if (expectedMetadata.client === "cursor"
731
+ && (!records.has("tools/hellolynk-install-client-skills.mjs")
732
+ || (expected.installedPreviousRelease !== true && (!records.has("tools/hellolynk-install-cursor-plugin.mjs")
733
+ || !records.has("cursor-plugin/server/hellolynk-mcp.js") || !records.has("cursor-plugin/bin/hellolynk-waiter.mjs")))
734
+ || !records.has("integrations/hellolynk-cursor/skills/hellolynk-messaging/SKILL.md"))) {
735
+ fail("hellolynk_npm_pack_invalid");
736
+ }
737
+ const workbench = manifest.workbench;
738
+ if (!workbench || typeof workbench !== "object" || Array.isArray(workbench)
739
+ || Object.keys(workbench).sort().join(",") !== "contract,manifestSha256,path,version"
740
+ || !safePackPath(workbench.path) || !/^[a-f0-9]{64}$/.test(workbench.manifestSha256)
741
+ || records.get(workbench.path)?.sha256 !== workbench.manifestSha256) fail("hellolynk_npm_pack_invalid");
742
+ return Object.freeze({ manifestSha256, fileCount: records.size });
743
+ }
744
+
745
+ function safePackPath(value) {
746
+ return typeof value === "string" && value !== PACK_MANIFEST && value !== PACK_PIN
747
+ && safeRelative(value) && !value.startsWith("./") && path.posix.normalize(value) === value
748
+ && value.split("/").every(safePackPathSegment);
749
+ }
750
+
751
+ function safePackPathSegment(value) {
752
+ const windowsBaseName = String(value).split(".")[0];
753
+ return typeof value === "string" && value.length >= 1 && value.length <= 255
754
+ && value !== "." && value !== ".." && !/[<>:"\\|?*\u0000-\u001F]/u.test(value)
755
+ && !/[. ]$/u.test(value)
756
+ && !/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/iu.test(windowsBaseName);
757
+ }
758
+
759
+ function portablePathKey(value) {
760
+ return typeof value === "string" ? value.normalize("NFC").toLowerCase() : "";
761
+ }
762
+
763
+ function registerPortablePackPath(nodes, value) {
764
+ const segments = value.split("/");
765
+ for (let index = 0; index < segments.length; index += 1) {
766
+ const portablePath = segments.slice(0, index + 1).join("/");
767
+ const kind = index === segments.length - 1 ? "file" : "directory";
768
+ const key = portablePathKey(portablePath);
769
+ const existing = nodes.get(key);
770
+ if (existing && (existing.path !== portablePath || existing.kind !== kind)) fail("hellolynk_npm_pack_invalid");
771
+ nodes.set(key, { path: portablePath, kind });
772
+ }
773
+ }
774
+
775
+ async function assertExactPackageTree(root, records, rootRealPath) {
776
+ const rootMetadata = await fs.lstat(root).catch(() => fail("hellolynk_npm_pack_invalid"));
777
+ if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) fail("hellolynk_npm_pack_invalid");
778
+ const expectedPaths = new Set([...records.keys(), PACK_MANIFEST, PACK_PIN]);
779
+ const actualPaths = new Set();
780
+
781
+ async function visit(directory, prefix = "") {
782
+ const directoryRealPath = await fs.realpath(directory).catch(() => fail("hellolynk_npm_pack_invalid"));
783
+ const relativeRealPath = path.relative(rootRealPath, directoryRealPath);
784
+ if (relativeRealPath === ".." || relativeRealPath.startsWith(`..${path.sep}`)
785
+ || path.isAbsolute(relativeRealPath)) fail("hellolynk_npm_pack_invalid");
786
+ const entries = await fs.readdir(directory, { withFileTypes: true })
787
+ .catch(() => fail("hellolynk_npm_pack_invalid"));
788
+ for (const entry of entries) {
789
+ if (!safePackPathSegment(entry.name)) fail("hellolynk_npm_pack_invalid");
790
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
791
+ const absolute = path.join(directory, entry.name);
792
+ const entryMetadata = await fs.lstat(absolute).catch(() => fail("hellolynk_npm_pack_invalid"));
793
+ if (entryMetadata.isSymbolicLink()) fail("hellolynk_npm_pack_invalid");
794
+ if (entryMetadata.isDirectory()) {
795
+ await visit(absolute, relative);
796
+ } else if (entryMetadata.isFile()) {
797
+ if (entryMetadata.nlink !== 1 || actualPaths.has(relative)) fail("hellolynk_npm_pack_invalid");
798
+ actualPaths.add(relative);
799
+ if (!expectedPaths.has(relative)) fail("hellolynk_npm_pack_invalid");
800
+ } else {
801
+ fail("hellolynk_npm_pack_invalid");
802
+ }
803
+ }
804
+ }
805
+
806
+ await visit(root);
807
+ if (actualPaths.size !== expectedPaths.size
808
+ || [...expectedPaths].some((relative) => !actualPaths.has(relative))) fail("hellolynk_npm_pack_invalid");
809
+ }
810
+
811
+ async function verifyRelease(releaseRoot, packageRoot, expectedMetadata = metadata) {
812
+ const runtime = path.join(releaseRoot, "runtime");
813
+ const manifestPath = path.join(runtime, "neutral-host-release-manifest.json");
814
+ const pin = await readJson(path.join(runtime, "neutral-host-release-pin.json"), "hellolynk_npm_release_invalid");
815
+ const bytes = await readRegularFile(manifestPath, "hellolynk_npm_release_invalid");
816
+ const manifestSha256 = sha256(bytes);
817
+ if (pin?.contract !== "hellolynk-neutral-host-release-pin-v1"
818
+ || Object.keys(pin).sort().join(",") !== "contract,manifestSha256"
819
+ || pin.manifestSha256 !== manifestSha256) fail("hellolynk_npm_release_invalid");
820
+ const manifest = JSON.parse(bytes.toString("utf8"));
821
+ if (manifest?.contract !== "hellolynk-neutral-host-release-manifest-v1"
822
+ || manifest.entry !== "neutral-host-release-entry.mjs" || !Array.isArray(manifest.files)
823
+ || manifest.files.length < 8) fail("hellolynk_npm_release_invalid");
824
+ let outerManifestSha256 = null;
825
+ if (expectedMetadata.outerManifestPin) {
826
+ const outerPin = await readJson(path.join(packageRoot, expectedMetadata.outerManifestPin), "hellolynk_npm_release_invalid");
827
+ const outerManifestPath = path.join(packageRoot, expectedMetadata.outerManifestPin.replace(/-pin\.json$/, "-manifest.json"));
828
+ outerManifestSha256 = sha256(await readRegularFile(outerManifestPath, "hellolynk_npm_release_invalid"));
829
+ if (outerPin?.manifestSha256 !== outerManifestSha256) fail("hellolynk_npm_release_invalid");
830
+ }
831
+ return Object.freeze({ manifestSha256, outerManifestSha256 });
832
+ }
833
+
834
+ function resolveLocations() {
835
+ if (process.platform !== "win32") fail("hellolynk_npm_windows_only");
836
+ const home = path.resolve(process.env.USERPROFILE || os.homedir());
837
+ const localAppData = path.resolve(process.env.LOCALAPPDATA || path.join(home, "AppData", "Local"));
838
+ const appData = path.resolve(process.env.APPDATA || path.join(home, "AppData", "Roaming"));
839
+ const configuredCodexHome = String(process.env.CODEX_HOME || "");
840
+ if (configuredCodexHome && !path.isAbsolute(configuredCodexHome)) fail("hellolynk_npm_codex_home_invalid");
841
+ const codexHome = path.resolve(configuredCodexHome || path.join(home, ".codex"));
842
+ const configuredHostRoot = String(process.env.HELLOLYNK_NEUTRAL_STORE_ROOT || "").trim();
843
+ if (configuredHostRoot && !path.isAbsolute(configuredHostRoot)) fail("hellolynk_npm_store_root_invalid");
844
+ const hostRoot = path.resolve(configuredHostRoot || path.join(localAppData, "HelloLynk", "host"));
845
+ if (configuredHostRoot && path.basename(hostRoot).toLowerCase() !== "host") {
846
+ fail("hellolynk_npm_store_root_invalid");
847
+ }
848
+ const finalCurrentRoot = path.dirname(hostRoot);
849
+ if (finalCurrentRoot === path.parse(finalCurrentRoot).root) fail("hellolynk_npm_store_root_invalid");
850
+ return Object.freeze({
851
+ home,
852
+ localAppData,
853
+ appData,
854
+ codexHome,
855
+ finalCurrentRoot,
856
+ hostRoot,
857
+ });
858
+ }
859
+
860
+ async function installStablePackage({ packageClosure, sourceRelease, locations }) {
861
+ const immutableId = `${metadata.client}-${packageClosure.manifestSha256}`;
862
+ const releasesRoot = path.join(locations.finalCurrentRoot, "releases");
863
+ const packageRoot = path.join(releasesRoot, immutableId);
864
+ const temporaryRoot = path.join(releasesRoot, `.${immutableId}.install-${crypto.randomUUID()}`);
865
+ await fs.mkdir(releasesRoot, { recursive: true, mode: 0o700 });
866
+
867
+ if (!(await exists(packageRoot))) {
868
+ try {
869
+ await fs.cp(PACKAGE_ROOT, temporaryRoot, {
870
+ recursive: true,
871
+ force: false,
872
+ errorOnExist: true,
873
+ preserveTimestamps: false,
874
+ });
875
+ await verifyPackageClosure(temporaryRoot, packageClosure.manifestSha256);
876
+ await secureStableReleaseTree(locations, path.basename(temporaryRoot));
877
+ for (let attempt = 0; attempt < 6; attempt += 1) {
878
+ try {
879
+ await fs.rename(temporaryRoot, packageRoot);
880
+ break;
881
+ } catch (error) {
882
+ const code = String(error?.code || "");
883
+ const harmlessRace = ["EEXIST", "ENOTEMPTY"].includes(code)
884
+ || (["EPERM", "EACCES"].includes(code) && await exists(packageRoot));
885
+ if (harmlessRace) break;
886
+ const transientWindowsLock = process.platform === "win32"
887
+ && ["EPERM", "EACCES"].includes(code) && attempt < 5;
888
+ if (!transientWindowsLock) throw error;
889
+ await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
890
+ }
891
+ }
892
+ } finally {
893
+ await fs.rm(temporaryRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
894
+ }
895
+ }
896
+
897
+ await secureStableReleaseTree(locations, immutableId, { verifyOnly: true })
898
+ .catch(() => fail("hellolynk_npm_stable_release_invalid"));
899
+ await verifyPackageClosure(packageRoot, packageClosure.manifestSha256)
900
+ .catch(() => fail("hellolynk_npm_stable_release_invalid"));
901
+ const releaseRoot = path.resolve(packageRoot, metadata.releaseRoot);
902
+ if (!isSameOrDescendant(releaseRoot, packageRoot)) fail("hellolynk_npm_stable_release_invalid");
903
+ const release = await verifyRelease(releaseRoot, packageRoot)
904
+ .catch(() => fail("hellolynk_npm_stable_release_invalid"));
905
+ if (release.manifestSha256 !== sourceRelease.manifestSha256
906
+ || release.outerManifestSha256 !== sourceRelease.outerManifestSha256) {
907
+ fail("hellolynk_npm_stable_release_invalid");
908
+ }
909
+ return Object.freeze({ packageRoot, releaseRoot, release });
910
+ }
911
+
912
+ async function inspectStablePackage({ packageClosure, sourceRelease, locations }) {
913
+ const immutableId = `${metadata.client}-${packageClosure.manifestSha256}`;
914
+ const packageRoot = path.join(locations.finalCurrentRoot, "releases", immutableId);
915
+ await secureStableReleaseTree(locations, immutableId, { verifyOnly: true })
916
+ .catch(() => fail("hellolynk_npm_stable_release_invalid"));
917
+ await verifyPackageClosure(packageRoot, packageClosure.manifestSha256)
918
+ .catch(() => fail("hellolynk_npm_stable_release_invalid"));
919
+ const releaseRoot = path.resolve(packageRoot, metadata.releaseRoot);
920
+ if (!isSameOrDescendant(releaseRoot, packageRoot)) fail("hellolynk_npm_stable_release_invalid");
921
+ const release = await verifyRelease(releaseRoot, packageRoot)
922
+ .catch(() => fail("hellolynk_npm_stable_release_invalid"));
923
+ if (release.manifestSha256 !== sourceRelease.manifestSha256
924
+ || release.outerManifestSha256 !== sourceRelease.outerManifestSha256) {
925
+ fail("hellolynk_npm_stable_release_invalid");
926
+ }
927
+ return Object.freeze({ packageRoot, releaseRoot, release });
928
+ }
929
+
930
+ async function prepareFinalClientRoot(locations) {
931
+ const module = await importCore(
932
+ "hellolynk-platform-credential-store.mjs",
933
+ "hellolynk_npm_client_root_security_unavailable",
934
+ );
935
+ try {
936
+ await module.prepareWindowsClientRoot(locations.finalCurrentRoot);
937
+ } catch (error) {
938
+ fail(/^[a-z][a-z0-9_]{2,127}$/.test(error?.code || "")
939
+ ? error.code : "hellolynk_npm_client_root_security_invalid");
940
+ }
941
+ }
942
+
943
+ async function requestInstalledHostHandover(context, { restore = false } = {}) {
944
+ let binding;
945
+ const publishedAt = new Date().toISOString();
946
+ try {
947
+ binding = await verifyExactBinding(context);
948
+ // Always use this verified installer's helper, including an explicit restore
949
+ // to a former package that did not yet provide the handover entry point.
950
+ const runtime = await importStableCore(PACKAGE_ROOT,
951
+ "hellolynk-neutral-host-process.mjs", "hellolynk_npm_host_handover_unavailable");
952
+ const request = {
953
+ runtimeRoot: binding.runtimeRoot,
954
+ hostInstallationId: binding.hostInstallationId,
955
+ publishedAt,
956
+ };
957
+ // An explicit restore went back to an earlier release: it removes this release's record and
958
+ // leaves whichever Host is running alone. It asks none to leave and starts none.
959
+ if (restore) {
960
+ await runtime.removeHelloLynkNeutralHostExpectedRelease({
961
+ runtimeRoot: request.runtimeRoot, hostInstallationId: request.hostInstallationId,
962
+ }).catch(() => {});
963
+ return { status: "not_requested" };
964
+ }
965
+ const result = await runtime.requestHelloLynkNeutralHostHandover(request);
966
+ // Only a Host that was running and made room is replaced. One started where there was
967
+ // none would have nothing attached to it and never leave.
968
+ if (["stopped", "restarted"].includes(result?.status)) {
969
+ await startHostWhenAbsent(runtime, binding).catch(() => {});
970
+ }
971
+ return result;
972
+ } catch {
973
+ // The install has committed. Failure to request a restart must not roll
974
+ // back valid configuration or report that installation itself failed.
975
+ return { status: "unconfirmed", code: "hellolynk_npm_host_handover_unconfirmed" };
976
+ } finally {
977
+ binding?.capability?.secretBuffer?.fill(0);
978
+ }
979
+ }
980
+
981
+ // A Host that left for this release is not started again until some client reconnects.
982
+ // Start one from the new binding unless another client has within two seconds.
983
+ async function startHostWhenAbsent(runtime, binding) {
984
+ const probe = {
985
+ region: binding.region,
986
+ principalId: binding.principalId,
987
+ hostInstallationId: binding.hostInstallationId,
988
+ runtimeRoot: binding.runtimeRoot,
989
+ };
990
+ for (let attempt = 0; attempt < 8; attempt += 1) {
991
+ if (await runtime.discoverHelloLynkNeutralHost(probe).catch(() => null)) return;
992
+ await new Promise((resolve) => setTimeout(resolve, 250));
993
+ }
994
+ const launcher = await importStableCore(PACKAGE_ROOT,
995
+ "hellolynk-neutral-host-launcher.mjs", "hellolynk_npm_host_handover_unavailable");
996
+ await launcher.launchPinnedNeutralHost(binding);
997
+ }
998
+
999
+ async function secureStableReleaseTree(locations, releaseId, options = {}) {
1000
+ const module = await importCore(
1001
+ "hellolynk-platform-credential-store.mjs",
1002
+ "hellolynk_npm_client_root_security_unavailable",
1003
+ );
1004
+ return module.secureWindowsClientReleaseTree(
1005
+ locations.finalCurrentRoot,
1006
+ releaseId,
1007
+ { verifyOnly: options.verifyOnly === true },
1008
+ );
1009
+ }
1010
+
1011
+ async function verifyExactBinding({ packageRoot, releaseRoot, release, locations, activation }) {
1012
+ const installationModule = await importStableCore(
1013
+ PACKAGE_ROOT,
1014
+ "hellolynk-neutral-host-installation-store.mjs",
1015
+ "hellolynk_npm_binding_invalid",
1016
+ );
1017
+ const protectedModule = await importStableCore(
1018
+ PACKAGE_ROOT,
1019
+ "hellolynk-host-protected-store.mjs",
1020
+ "hellolynk_npm_binding_invalid",
1021
+ );
1022
+ const verifyPath = ({ path: target, directory }) => protectedModule.verifyHostProtectedStorePath({
1023
+ root: locations.hostRoot,
1024
+ path: target,
1025
+ directory,
1026
+ });
1027
+ const installationStore = installationModule.createNeutralHostInstallationStore({
1028
+ root: locations.hostRoot,
1029
+ readOnly: true,
1030
+ verifyPath,
1031
+ });
1032
+ const installationStatus = await installationStore.status();
1033
+ const activeInstallations = installationStatus.installations.filter((item) => item.state === "active");
1034
+ if (activeInstallations.length !== 1) fail("hellolynk_npm_binding_invalid");
1035
+ const installation = activeInstallations[0];
1036
+ const store = protectedModule.createHostProtectedStore({
1037
+ root: locations.hostRoot,
1038
+ installationStore,
1039
+ readOnly: true,
1040
+ });
1041
+ const binding = metadata.client === "codex"
1042
+ ? await store.verifyCodexNeutralBinding()
1043
+ : await store[{
1044
+ openclaw: "verifyOpenClawNeutralBinding",
1045
+ "trae-work": "verifyTraeWorkNeutralBinding",
1046
+ workbuddy: "verifyWorkBuddyNeutralBinding",
1047
+ cursor: "verifyCursorNeutralBinding",
1048
+ dsh: "verifyDshNeutralBinding",
1049
+ }[metadata.client]]({ profileId: metadata.profileId });
1050
+ // Parse the shared store with the pinned candidate, but keep the target
1051
+ // package's exact contract. A newer sibling does not change this target's scopes.
1052
+ const targetModule = await importStableCore(packageRoot, "hellolynk-host-protected-store.mjs", "hellolynk_npm_binding_invalid");
1053
+ const expectedScopes = targetModule[{
1054
+ codex: "HELLOLYNK_CODEX_NEUTRAL_EXACT_SCOPES",
1055
+ openclaw: "HELLOLYNK_OPENCLAW_NEUTRAL_EXACT_SCOPES",
1056
+ "trae-work": "HELLOLYNK_TRAE_WORK_NEUTRAL_EXACT_SCOPES",
1057
+ workbuddy: "HELLOLYNK_WORKBUDDY_NEUTRAL_EXACT_SCOPES",
1058
+ cursor: "HELLOLYNK_CURSOR_NEUTRAL_EXACT_SCOPES",
1059
+ dsh: "HELLOLYNK_DSH_NEUTRAL_EXACT_SCOPES",
1060
+ }[metadata.client]];
1061
+ const transport = await store.verifyTransport({
1062
+ principalId: installation.principalId,
1063
+ hostInstallationId: installation.hostInstallationId,
1064
+ });
1065
+ if (binding?.adapterId !== metadata.client || binding?.profileId !== metadata.profileId
1066
+ || binding?.version !== 2 || binding?.capability?.state !== "active"
1067
+ || binding.capability?.version !== 2
1068
+ || path.resolve(binding.runtimeRoot || ".") !== path.join(locations.hostRoot, "runtime")
1069
+ || path.resolve(binding.releaseRoot || ".") !== releaseRoot
1070
+ || binding.releaseManifestSha256 !== release.manifestSha256
1071
+ || binding.runnerRelativePath !== "runtime/neutral-host-bootstrap.mjs"
1072
+ || path.resolve(binding.profileManifestPath || ".") !== activation.profileManifest
1073
+ || !sameStrings(binding.scopes, expectedScopes)
1074
+ || !sameStrings(binding.capability?.scopes, expectedScopes)
1075
+ || binding.principalId !== installation.principalId
1076
+ || binding.hostInstallationId !== installation.hostInstallationId
1077
+ || binding.capability?.principalId !== installation.principalId
1078
+ || binding.capability?.hostInstallationId !== installation.hostInstallationId
1079
+ || transport.credential?.principalId !== installation.principalId
1080
+ || transport.credential?.hostInstallationId !== installation.hostInstallationId
1081
+ || (transport.session !== null && (
1082
+ transport.session.principalId !== installation.principalId
1083
+ || transport.session.hostInstallationId !== installation.hostInstallationId
1084
+ ))) {
1085
+ fail("hellolynk_npm_binding_invalid");
1086
+ }
1087
+ return binding;
1088
+ }
1089
+
1090
+ function bindingOwner(binding) {
1091
+ return Object.fromEntries(["region", "principalId", "delegateId", "hostInstallationId", "profileId", "adapterId", "version", "runtimeRoot"]
1092
+ .map((key) => [key, binding[key]]));
1093
+ }
1094
+
1095
+ async function verifyClientConfiguration(context, { verifyNativeService = true } = {}) {
1096
+ // The native binding and Cursor's explicit-setup Skill are separate checks.
1097
+ // Doctor never installs a missing Skill or writes global client settings.
1098
+ if (isNativeClient()) {
1099
+ if (metadata.client === "cursor") {
1100
+ const skill = await preflightCursorSkill(context);
1101
+ await skill.module.verifyInstalledClientSkill({ source: skill.plan.targets[0].source,
1102
+ destination: skill.plan.targets[0].destination, trustedAnchor: skill.plan.targets[0].trustedAnchor });
1103
+ const plugin = await preflightCursorPlugin(context, { verifyOnly: true });
1104
+ await plugin.module.verifyCursorPlugin(plugin.plan);
1105
+ }
1106
+ return true;
1107
+ }
1108
+ if (metadata.client === "codex") return verifyCodexConfiguration(context);
1109
+ if (metadata.client === "openclaw") return verifyOpenClawConfiguration(context);
1110
+ await verifyMcpConfiguration(context);
1111
+ if (metadata.client === "workbuddy") {
1112
+ await configureWorkBuddyPageTask(context, "Verify");
1113
+ if (verifyNativeService) await updateWorkBuddyNativeService(context, "Verify");
1114
+ }
1115
+ return true;
1116
+ }
1117
+
1118
+ async function configureWorkBuddyPageTask({ packageRoot, releaseRoot, release, locations }, operation) {
1119
+ // Keep all previous-package byte/config checks, but inspect its task through
1120
+ // this verified installer's driver instead of an obsolete CIM query.
1121
+ const module = await importLocal(operation === "Verify" ? PACKAGE_ROOT : packageRoot, "integrations/workbuddy/server/workbench-process.mjs", "hellolynk_npm_workbuddy_page_service_invalid");
1122
+ return module.configureWorkBuddyWorkbenchTask({ root: releaseRoot, expectedManifestSha256: release.outerManifestSha256, storeRoot: locations.hostRoot }, operation);
1123
+ }
1124
+
1125
+ async function updateWorkBuddyNativeService({ releaseRoot, release, locations }, operation, installIfMissing = false) {
1126
+ // Use the current installer for a rollback target too. Older immutable
1127
+ // packages may predate this reconciliation entry point.
1128
+ const module = await importLocal(PACKAGE_ROOT, "integrations/workbuddy/server/workbench-process.mjs", "hellolynk_npm_workbuddy_page_service_invalid");
1129
+ return module.updateRecordedWorkBuddyNativeTaskService({ root: releaseRoot,
1130
+ expectedManifestSha256: release.outerManifestSha256, storeRoot: locations.hostRoot, installIfMissing }, operation);
1131
+ }
1132
+
1133
+ async function verifyCodexConfiguration({ packageRoot, releaseRoot, locations }) {
1134
+ const marketplace = "hellolynk-npm-codex";
1135
+ const code = "hellolynk_npm_config_invalid";
1136
+ const configPath = path.join(locations.codexHome, "config.toml");
1137
+ await assertCanonicalDirectoryChain(locations.codexHome, path.dirname(configPath), code);
1138
+ const configBytes = await readRegularFile(configPath, code, true);
1139
+ if (configBytes.length > MAX_JSON_BYTES) fail(code);
1140
+ const tables = parseCodexConfiguration(configBytes.toString("utf8"), code);
1141
+ if (tables.marketplace.sourceType !== "local"
1142
+ || !path.isAbsolute(String(tables.marketplace.source || ""))
1143
+ || !samePath(tables.marketplace.source, packageRoot)
1144
+ || tables.plugin.enabled !== true) fail(code);
1145
+
1146
+ const sourceManifest = await readJson(
1147
+ path.join(releaseRoot, ".codex-plugin", "plugin.json"),
1148
+ code,
1149
+ true,
1150
+ );
1151
+ const version = String(sourceManifest?.version || "");
1152
+ if (sourceManifest?.name !== "hellolynk-codex"
1153
+ || !/^[A-Za-z0-9][A-Za-z0-9.+_-]{0,127}$/u.test(version)) fail(code);
1154
+ const cacheParent = path.join(
1155
+ locations.codexHome,
1156
+ "plugins",
1157
+ "cache",
1158
+ marketplace,
1159
+ "hellolynk-codex",
1160
+ );
1161
+ await assertCanonicalDirectoryChain(locations.codexHome, cacheParent, code);
1162
+ // Windows may retain a locked, empty directory from a removed version. Check
1163
+ // the version Codex actually selects, then verify every installed byte.
1164
+ const plugins = runCodexJson(
1165
+ ["plugin", "list", "--marketplace", marketplace, "--json"], code, locations.codexHome,
1166
+ );
1167
+ if (!Array.isArray(plugins?.installed)) fail(code);
1168
+ const selected = plugins.installed.filter((item) => item?.pluginId === `hellolynk-codex@${marketplace}`);
1169
+ if (selected.length !== 1 || selected[0].enabled !== true || selected[0].version !== version
1170
+ || !samePath(String(selected[0].source?.path || ""), releaseRoot)) fail(code);
1171
+ const cacheRoot = path.join(cacheParent, version);
1172
+ await verifyExactRegularTree(releaseRoot, cacheRoot, locations.codexHome, code);
1173
+ return true;
1174
+ }
1175
+
1176
+ async function verifyOpenClawConfiguration({ packageRoot, locations }) {
1177
+ const config = await readJson(
1178
+ path.join(locations.home, ".openclaw", "openclaw.json"),
1179
+ "hellolynk_npm_config_invalid",
1180
+ );
1181
+ const channel = config?.channels?.hellolynk;
1182
+ const configurator = await importLocal(packageRoot, metadata.configurator, "hellolynk_npm_configurator_invalid");
1183
+ let expectedChannel;
1184
+ try { expectedChannel = configurator.buildOpenClawChannelConfig(channel?.nativeModel); }
1185
+ catch { fail("hellolynk_npm_config_invalid"); }
1186
+ const pluginPath = path.resolve(packageRoot, metadata.pluginPath);
1187
+ const paths = config?.plugins?.load?.paths;
1188
+ const helloLynkPluginPaths = [];
1189
+ const legacyPluginPaths = [];
1190
+ if (Array.isArray(paths)) {
1191
+ for (const configuredPath of paths) {
1192
+ const absolute = path.resolve(String(configuredPath || ""));
1193
+ const manifest = await readJson(path.join(absolute, "openclaw.plugin.json"), "hellolynk_npm_config_invalid")
1194
+ .catch(() => null);
1195
+ if (manifest?.id === "hellolynk") helloLynkPluginPaths.push(absolute);
1196
+ if (isLegacyOpenClawPluginPath(absolute)) legacyPluginPaths.push(absolute);
1197
+ }
1198
+ }
1199
+ if (!channel || Object.keys(channel).sort().join(",") !== Object.keys(expectedChannel).sort().join(",")
1200
+ || channel.enabled !== true || channel.hostMode !== "neutral"
1201
+ || channel.bindingRef !== "protected-store:openclaw"
1202
+ || !Array.isArray(paths) || paths.filter((item) => path.resolve(String(item || "")) === pluginPath).length !== 1
1203
+ || helloLynkPluginPaths.length !== 1 || helloLynkPluginPaths[0] !== pluginPath
1204
+ || legacyPluginPaths.length !== 0
1205
+ || Object.hasOwn(config?.channels || {}, "lynk")
1206
+ || Object.hasOwn(config?.plugins?.entries || {}, "lynk")
1207
+ || config?.plugins?.entries?.hellolynk?.enabled !== true) {
1208
+ fail("hellolynk_npm_config_invalid");
1209
+ }
1210
+ return true;
1211
+ }
1212
+
1213
+ async function verifyMcpConfiguration({ packageRoot, releaseRoot, release, locations }) {
1214
+ const configPaths = metadata.client === "trae-work"
1215
+ ? await traeConfigPaths(locations.appData)
1216
+ : await workBuddyConfigPaths(locations.home);
1217
+ const entry = path.join(packageRoot, metadata.entry);
1218
+ const expectedArgs = metadata.client === "trae-work"
1219
+ ? [entry, "run", "--expected-manifest-sha256", release.manifestSha256]
1220
+ : [entry, "run", "--root", releaseRoot, "--expected-manifest-sha256", release.outerManifestSha256];
1221
+ for (const configPath of configPaths) {
1222
+ const config = await readJson(configPath, "hellolynk_npm_config_invalid");
1223
+ const server = config?.mcpServers?.hellolynk;
1224
+ if (!isHelloLynkMcpServerShape(metadata.client, server)
1225
+ || path.resolve(String(server.command || "")) !== path.resolve(process.execPath)
1226
+ || !sameStrings(server.args, expectedArgs)
1227
+ || Object.keys(server.env || {}).sort().join(",") !== "HELLOLYNK_NEUTRAL_STORE_ROOT"
1228
+ || path.resolve(String(server.env.HELLOLYNK_NEUTRAL_STORE_ROOT || "")) !== locations.hostRoot
1229
+ || Object.hasOwn(config.mcpServers, "lynk")) fail("hellolynk_npm_config_invalid");
1230
+ }
1231
+ if (metadata.skillTarget) {
1232
+ // Check the installed skill with this verified installer's path rules;
1233
+ // packageRoot still supplies the previously pinned expected skill bytes.
1234
+ const skillModule = await importLocal(
1235
+ PACKAGE_ROOT,
1236
+ "tools/hellolynk-install-client-skills.mjs",
1237
+ "hellolynk_npm_skill_installer_invalid",
1238
+ );
1239
+ const plan = await skillModule.installClientSkills({
1240
+ sourceRoot: packageRoot,
1241
+ home: locations.home,
1242
+ appData: locations.appData,
1243
+ localAppData: locations.localAppData,
1244
+ target: metadata.skillTarget,
1245
+ apply: false,
1246
+ });
1247
+ if (!Array.isArray(plan?.targets) || plan.targets.length !== 1) fail("hellolynk_npm_config_invalid");
1248
+ await skillModule.verifyInstalledClientSkill({
1249
+ source: plan.targets[0].source,
1250
+ destination: plan.targets[0].destination,
1251
+ trustedAnchor: plan.targets[0].trustedAnchor,
1252
+ })
1253
+ .catch(() => fail("hellolynk_npm_config_invalid"));
1254
+ }
1255
+ return true;
1256
+ }
1257
+
1258
+ async function importStableCore(packageRoot, name, code) {
1259
+ return importLocal(packageRoot, path.posix.join(metadata.coreRoot.replaceAll("\\", "/"), name), code);
1260
+ }
1261
+
1262
+ function sameStrings(left, right) {
1263
+ return Array.isArray(left) && Array.isArray(right)
1264
+ && left.length === right.length && left.every((value, index) => value === right[index]);
1265
+ }
1266
+
1267
+ function isSameOrDescendant(candidate, root) {
1268
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
1269
+ return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
1270
+ }
1271
+
1272
+ async function inspectActivation(hostRoot, profileId) {
1273
+ const profileManifest = path.join(hostRoot, "profile-manifest-v1.json");
1274
+ let value;
1275
+ try { value = await readJson(profileManifest, "hellolynk_npm_existing_host_activation_required"); }
1276
+ catch { return Object.freeze({ ready: false, state: "missing", profileManifest }); }
1277
+ if (value?.contract !== "hellolynk-neutral-host-profile-manifest-v1" || !Array.isArray(value.profiles)) {
1278
+ return Object.freeze({ ready: false, state: "invalid", profileManifest });
1279
+ }
1280
+ const ready = value.profiles.some((item) => path.basename(String(item || "")) === `${profileId}.json`);
1281
+ return Object.freeze({ ready, state: ready ? "ready" : "client_profile_missing", profileManifest });
1282
+ }
1283
+
1284
+ async function initializeHost(locations, packageRoot) {
1285
+ const module = await importStableCore(
1286
+ packageRoot,
1287
+ "hellolynk-neutral-host-installation-store.mjs",
1288
+ "hellolynk_npm_activation_runtime_invalid",
1289
+ );
1290
+ return module.createNeutralHostInstallationStore({ root: locations.hostRoot }).createId();
1291
+ }
1292
+
1293
+ async function hasHostTransport(locations, packageRoot) {
1294
+ const installationModule = await importStableCore(
1295
+ packageRoot,
1296
+ "hellolynk-neutral-host-installation-store.mjs",
1297
+ "hellolynk_npm_activation_runtime_invalid",
1298
+ );
1299
+ const protectedModule = await importStableCore(
1300
+ packageRoot,
1301
+ "hellolynk-host-protected-store.mjs",
1302
+ "hellolynk_npm_activation_runtime_invalid",
1303
+ );
1304
+ const installationStore = installationModule.createNeutralHostInstallationStore({ root: locations.hostRoot });
1305
+ const store = protectedModule.createHostProtectedStore({ root: locations.hostRoot, installationStore });
1306
+ try {
1307
+ await store.resolveTransport();
1308
+ return true;
1309
+ } catch (error) {
1310
+ if (String(error?.code || error?.message) === "host_transport_credential_required") return false;
1311
+ throw error;
1312
+ }
1313
+ }
1314
+
1315
+ async function importCore(name, code) {
1316
+ const coreRoot = path.resolve(PACKAGE_ROOT, metadata.coreRoot);
1317
+ const file = path.resolve(coreRoot, name);
1318
+ if (path.dirname(file) !== coreRoot) fail(code);
1319
+ await readRegularFile(file, code);
1320
+ return import(pathToFileURL(file).href);
1321
+ }
1322
+
1323
+ async function activateBinding(context) {
1324
+ const { packageRoot, releaseRoot, release, locations, activation } = context;
1325
+ if (["codex", "dsh"].includes(metadata.client) && context.packageClosure
1326
+ && (context.recoveryPrevious || context.packageClosure.manifestSha256 !== candidateClosure.manifestSha256)) {
1327
+ return activateCompatibleRecoveryBinding(context);
1328
+ }
1329
+ const tool = path.join(packageRoot, metadata.activationTool);
1330
+ await readRegularFile(tool, "hellolynk_npm_activation_tool_invalid");
1331
+ const result = spawnSync(process.execPath, [
1332
+ tool,
1333
+ "--client", metadata.client,
1334
+ "--final-current-root", locations.finalCurrentRoot,
1335
+ "--root", locations.hostRoot,
1336
+ "--release-current-root", locations.releaseCurrentRoot || locations.finalCurrentRoot,
1337
+ "--release-root", releaseRoot,
1338
+ "--expected-release-manifest-sha256", release.manifestSha256,
1339
+ "--profile-manifest", activation.profileManifest,
1340
+ ], { encoding: "utf8", windowsHide: true, shell: false, env: minimalEnv() });
1341
+ if (result.status !== 0) fail(codeFromOutput(result.stderr, "hellolynk_npm_binding_activation_failed"));
1342
+ }
1343
+
1344
+ async function activateCompatibleRecoveryBinding(context) {
1345
+ // The previous package is immutable. Never run its obsolete shared-store
1346
+ // parser or accept a caller-supplied helper. Recheck both pinned closures
1347
+ // immediately before this recovery mutation.
1348
+ await verifyPackageClosure(PACKAGE_ROOT, candidateClosure.manifestSha256);
1349
+ const target = await inspectTrustedPackageRoot({ ...context,
1350
+ currentPackageRoot: path.join(path.dirname(context.packageRoot), `${metadata.client}-${candidateClosure.manifestSha256}`) });
1351
+ if (target.packageClosure.manifestSha256 !== context.packageClosure.manifestSha256
1352
+ || target.release.manifestSha256 !== context.release.manifestSha256) fail("hellolynk_npm_previous_install_invalid");
1353
+ const anchorContext = context.recoveryPrevious || context;
1354
+ const anchor = await inspectTrustedPackageRoot({ ...anchorContext,
1355
+ currentPackageRoot: path.join(path.dirname(context.packageRoot), `${metadata.client}-${candidateClosure.manifestSha256}`) });
1356
+ if (anchor.packageClosure.manifestSha256 !== anchorContext.packageClosure.manifestSha256
1357
+ || anchor.release.manifestSha256 !== anchorContext.release.manifestSha256) fail("hellolynk_npm_previous_install_invalid");
1358
+ const reader = await importStableCore(PACKAGE_ROOT, "hellolynk-host-protected-store.mjs", "hellolynk_npm_binding_invalid");
1359
+ const targetModule = await importStableCore(target.packageRoot, "hellolynk-host-protected-store.mjs", "hellolynk_npm_binding_invalid");
1360
+ const scopes = targetModule[`HELLOLYNK_${metadata.client.toUpperCase()}_NEUTRAL_EXACT_SCOPES`];
1361
+ const anchorModule = await importStableCore(anchor.packageRoot, "hellolynk-host-protected-store.mjs", "hellolynk_npm_binding_invalid");
1362
+ const anchorScopes = anchorModule[`HELLOLYNK_${metadata.client.toUpperCase()}_NEUTRAL_EXACT_SCOPES`];
1363
+ const exact = reader[`HELLOLYNK_${metadata.client.toUpperCase()}_NEUTRAL_EXACT_SCOPES`];
1364
+ const previousScopes = exact.filter((scope) => scope !== "app-data:authorized-messaging");
1365
+ if ([scopes, anchorScopes].some((value) => !sameStrings(value, exact) && !sameStrings(value, previousScopes))) {
1366
+ fail("hellolynk_npm_previous_scope_contract_invalid");
1367
+ }
1368
+ const store = reader.createHostProtectedStore({ root: context.locations.hostRoot, readOnly: context.recoveryCheckOnly === true });
1369
+ const suffix = metadata.client === "codex" ? "Codex" : "Dsh";
1370
+ const binding = await store[`verify${suffix}NeutralBinding`]({ profileId: metadata.profileId });
1371
+ const keys = ["contract", "region", "principalId", "delegateId", "hostInstallationId", "profileId", "adapterId", "version",
1372
+ "scopes", "runtimeRoot", "releaseRoot", "releaseManifestSha256", "runnerRelativePath", "profileManifestPath"];
1373
+ const previousBinding = Object.fromEntries(keys.map((key) => [key, binding[key]]));
1374
+ const candidateReleaseRoot = path.resolve(path.dirname(target.packageRoot),
1375
+ `${metadata.client}-${candidateClosure.manifestSha256}`, metadata.releaseRoot);
1376
+ const candidateRelease = await verifyRelease(path.resolve(PACKAGE_ROOT, metadata.releaseRoot), PACKAGE_ROOT);
1377
+ if (binding.profileId !== metadata.profileId || binding.adapterId !== metadata.client
1378
+ || !context.expectedOwner || stableJson(bindingOwner(binding)) !== stableJson(context.expectedOwner)
1379
+ || !samePath(binding.runtimeRoot, path.join(context.locations.hostRoot, "runtime"))
1380
+ || !((samePath(binding.releaseRoot, anchor.releaseRoot) && binding.releaseManifestSha256 === anchor.release.manifestSha256
1381
+ && sameStrings(binding.scopes, anchorScopes))
1382
+ || (samePath(binding.releaseRoot, candidateReleaseRoot) && binding.releaseManifestSha256 === candidateRelease.manifestSha256
1383
+ && sameStrings(binding.scopes, exact)))) {
1384
+ fail("hellolynk_npm_binding_invalid");
1385
+ }
1386
+ if (context.recoveryCheckOnly === true) return;
1387
+ const replacementBinding = { ...previousBinding, scopes: [...scopes], releaseRoot: target.releaseRoot,
1388
+ releaseManifestSha256: target.release.manifestSha256, runnerRelativePath: "runtime/neutral-host-bootstrap.mjs",
1389
+ profileManifestPath: context.activation.profileManifest };
1390
+ if (stableJson(previousBinding) === stableJson(replacementBinding)) return;
1391
+ if (metadata.client === "codex" && sameStrings(binding.scopes, scopes)) {
1392
+ await store.replaceCodexNeutralBinding({ previousBinding, replacementBinding: { ...replacementBinding, allowLegacyScopes: true } });
1393
+ } else {
1394
+ const method = metadata.client === "dsh" && sameStrings(scopes, previousScopes)
1395
+ ? "restoreDshNeutralBinding" : `migrate${suffix}NeutralBinding`;
1396
+ const migrated = await store[method]({ previousBinding,
1397
+ replacementBinding: { ...replacementBinding, ...(metadata.client === "codex" ? { allowLegacyScopes: true } : {}) },
1398
+ previousCapabilityId: binding.capability.capabilityId });
1399
+ migrated.capability?.secretBuffer?.fill?.(0);
1400
+ }
1401
+ }
1402
+
1403
+ async function preflightClientConfiguration(context) {
1404
+ if (isNativeClient()) return Object.freeze({ kind: "native", profileId: metadata.profileId,
1405
+ bindingRef: `protected-store:${metadata.client}`,
1406
+ ...(metadata.client === "cursor" ? { skill: await preflightCursorSkill(context), plugin: await preflightCursorPlugin(context) } : {}) });
1407
+ if (metadata.client === "codex") return preflightCodexMarketplace(context);
1408
+ if (metadata.client === "openclaw") return preflightOpenClaw(context);
1409
+ return preflightMcpClient(context);
1410
+ }
1411
+
1412
+ async function preflightCursorSkill({ packageRoot, locations, previousCursorPackage }) {
1413
+ const module = await importLocal(PACKAGE_ROOT, "tools/hellolynk-install-client-skills.mjs", "hellolynk_npm_skill_installer_invalid");
1414
+ const options = Object.freeze({ sourceRoot: packageRoot, home: locations.home,
1415
+ appData: locations.appData, localAppData: locations.localAppData, target: "cursor",
1416
+ ...(previousCursorPackage ? { previousSourceRoot: previousCursorPackage.packageRoot } : {}) });
1417
+ const plan = await module.installClientSkills({ ...options, apply: false });
1418
+ if (!/^[a-f0-9]{64}$/.test(String(plan?.fingerprint || ""))
1419
+ || plan.targets?.length !== 1 || plan.targets[0].target !== "cursor") {
1420
+ fail("hellolynk_npm_skill_installer_invalid");
1421
+ }
1422
+ return Object.freeze({ module, options, plan });
1423
+ }
1424
+
1425
+ async function preflightCursorPlugin(context, { verifyOnly = false } = {}) {
1426
+ const module = await importLocal(PACKAGE_ROOT, "tools/hellolynk-install-cursor-plugin.mjs", "hellolynk_npm_cursor_plugin_installer_invalid");
1427
+ const plan = await module.prepareCursorPlugin({ sourceRoot: context.packageRoot, home: context.locations.home,
1428
+ appData: context.locations.appData, localAppData: context.locations.localAppData, nodePath: process.execPath,
1429
+ migrateExistingPermissions: migrateCursorPermissions && !verifyOnly, useInstalledNode: verifyOnly,
1430
+ // Only the already verified previous package may lack the new plugin.
1431
+ allowLegacy: context.installedPreviousRelease === true });
1432
+ return { module, plan };
1433
+ }
1434
+
1435
+ async function preflightBindingActivation({
1436
+ packageRoot, releaseRoot, release, locations, activation, preBind = false,
1437
+ }) {
1438
+ const tool = path.join(packageRoot, metadata.activationTool);
1439
+ await readRegularFile(tool, "hellolynk_npm_activation_tool_invalid");
1440
+ const result = spawnSync(process.execPath, [
1441
+ tool,
1442
+ "--client", metadata.client,
1443
+ "--final-current-root", locations.finalCurrentRoot,
1444
+ "--root", locations.hostRoot,
1445
+ "--release-current-root", locations.releaseCurrentRoot || locations.finalCurrentRoot,
1446
+ "--release-root", releaseRoot,
1447
+ "--expected-release-manifest-sha256", release.manifestSha256,
1448
+ "--profile-manifest", activation.profileManifest,
1449
+ "--preflight", "true",
1450
+ ...(preBind ? ["--pre-bind", "true"] : []),
1451
+ ], { encoding: "utf8", windowsHide: true, shell: false, env: minimalEnv() });
1452
+ if (result.status !== 0) {
1453
+ fail(codeFromOutput(result.stderr, "hellolynk_npm_binding_preflight_failed"));
1454
+ }
1455
+ let verified;
1456
+ try { verified = JSON.parse(result.stdout); }
1457
+ catch { fail("hellolynk_npm_binding_preflight_invalid"); }
1458
+ const clients = Array.isArray(verified?.clients) ? verified.clients : [];
1459
+ if (verified?.ok !== true || verified.preflight !== true || verified.preBind !== preBind
1460
+ || verified.readOnly !== true
1461
+ || verified.protected !== true || verified.releaseManifestSha256 !== release.manifestSha256
1462
+ || !Number.isInteger(verified.verifiedReleaseFileCount) || verified.verifiedReleaseFileCount < 8
1463
+ || clients.length !== 1 || clients[0]?.adapterId !== metadata.client
1464
+ || clients[0]?.bindingUpdated !== false) {
1465
+ fail("hellolynk_npm_binding_preflight_invalid");
1466
+ }
1467
+ const freshPreBind = preBind && verified.installationState === "unclaimed";
1468
+ if (freshPreBind) {
1469
+ if (verified.profileManifestState !== "deferred"
1470
+ || verified.bootstrapTransportRequired !== true
1471
+ || verified.transportCredentialState !== "bootstrap_required"
1472
+ || clients[0].profileId !== null
1473
+ || clients[0].delegatePresent !== false
1474
+ || clients[0].bindingPresent !== false
1475
+ || clients[0].bindingState !== "awaiting_delegate_binding"
1476
+ || clients[0].plannedMutation !== "deferred") {
1477
+ fail("hellolynk_npm_binding_preflight_invalid");
1478
+ }
1479
+ } else if (verified.installationState !== "active"
1480
+ || verified.profileManifestState !== "verified"
1481
+ || verified.bootstrapTransportRequired !== false
1482
+ || verified.transportCredentialState !== "active") {
1483
+ fail("hellolynk_npm_binding_preflight_invalid");
1484
+ }
1485
+ if (isNativeClient() && clients[0].profileId !== null && clients[0].profileId !== metadata.profileId) {
1486
+ fail("hellolynk_native_existing_profile_conflict");
1487
+ }
1488
+ if (isNativeClient() && !freshPreBind && clients[0].profileId === null
1489
+ && (!preBind || clients[0].delegatePresent !== false || clients[0].bindingPresent !== false
1490
+ || clients[0].bindingState !== "awaiting_delegate_binding" || clients[0].plannedMutation !== "deferred")) {
1491
+ fail("hellolynk_npm_binding_preflight_invalid");
1492
+ }
1493
+ return Object.freeze({
1494
+ ready: true,
1495
+ freshPreBind,
1496
+ bindingState: String(clients[0].bindingState || "unknown"),
1497
+ });
1498
+ }
1499
+
1500
+ async function configureClient(context, prepared, { bindingContext = context } = {}) {
1501
+ if (isNativeClient()) {
1502
+ if (prepared?.kind !== "native" || prepared.profileId !== metadata.profileId
1503
+ || prepared.bindingRef !== `protected-store:${metadata.client}`) fail("hellolynk_npm_configuration_preflight_required");
1504
+ const activation = await inspectActivation(context.locations.hostRoot, metadata.profileId);
1505
+ await verifyExactBinding({ ...bindingContext, activation });
1506
+ let skillReceipt, pluginReceipt;
1507
+ if (metadata.client === "cursor") {
1508
+ if (!prepared.skill || !prepared.plugin) fail("hellolynk_npm_configuration_preflight_required");
1509
+ const installed = await prepared.skill.module.installClientSkills({ ...prepared.skill.options,
1510
+ apply: true, expectedPlanFingerprint: prepared.skill.plan.fingerprint });
1511
+ skillReceipt = installed.targets[0].receipt;
1512
+ if (skillReceipt?.contract !== "hellolynk-cursor-skill-install-receipt-v1") {
1513
+ fail("hellolynk_npm_skill_installer_invalid");
1514
+ }
1515
+ pluginReceipt = await prepared.plugin.module.installCursorPlugin(prepared.plugin.plan);
1516
+ }
1517
+ return Object.freeze({ bindingRef: prepared.bindingRef, profileId: metadata.profileId,
1518
+ ...(skillReceipt ? { skillInstalled: true, skillReceipt, pluginReceipt } : {}) });
1519
+ }
1520
+ if (metadata.client === "codex") return configureCodexMarketplace(context, prepared);
1521
+ if (metadata.client === "openclaw") return configureOpenClaw(context, prepared);
1522
+ return configureMcpClient(context, prepared);
1523
+ }
1524
+
1525
+ async function preflightCodexMarketplace({ packageRoot, locations }) {
1526
+ const marketplace = "hellolynk-npm-codex";
1527
+ const listed = runCodexJson(
1528
+ ["plugin", "marketplace", "list", "--json"],
1529
+ "hellolynk_npm_codex_marketplace_list_failed",
1530
+ locations.codexHome,
1531
+ );
1532
+ if (!Array.isArray(listed?.marketplaces)) fail("hellolynk_npm_codex_marketplace_list_invalid");
1533
+ const matches = listed.marketplaces.filter((item) => item?.name === marketplace);
1534
+ if (matches.length > 1) fail("hellolynk_npm_codex_marketplace_list_invalid");
1535
+ const existing = matches[0] || null;
1536
+ if (existing && path.resolve(String(existing.root || "")) !== packageRoot) {
1537
+ const previous = await inspectTrustedCodexInstallation({
1538
+ packageRoot: path.resolve(String(existing.root || "")),
1539
+ currentPackageRoot: packageRoot,
1540
+ locations,
1541
+ });
1542
+ return Object.freeze({
1543
+ kind: "codex",
1544
+ marketplace,
1545
+ existing: true,
1546
+ upgrade: true,
1547
+ previous,
1548
+ });
1549
+ }
1550
+ return Object.freeze({ kind: "codex", marketplace, existing: existing !== null, upgrade: false });
1551
+ }
1552
+
1553
+ async function configureCodexMarketplace(context, prepared) {
1554
+ const { packageRoot, locations } = context;
1555
+ if (prepared?.kind !== "codex") fail("hellolynk_npm_configuration_preflight_required");
1556
+ if (prepared.upgrade === true) fail("hellolynk_npm_codex_upgrade_transaction_required");
1557
+ const marketplace = prepared.marketplace;
1558
+ const listed = runCodexJson(
1559
+ ["plugin", "marketplace", "list", "--json"],
1560
+ "hellolynk_npm_codex_marketplace_list_failed",
1561
+ locations.codexHome,
1562
+ );
1563
+ if (!Array.isArray(listed?.marketplaces)) fail("hellolynk_npm_codex_marketplace_list_invalid");
1564
+ const matches = listed.marketplaces.filter((item) => item?.name === marketplace);
1565
+ if (matches.length > 1) fail("hellolynk_npm_codex_marketplace_list_invalid");
1566
+ const existing = matches[0] || null;
1567
+ if (existing && !samePath(String(existing.root || ""), packageRoot)) {
1568
+ fail("hellolynk_npm_configuration_changed_since_preflight");
1569
+ }
1570
+ if (!existing) runCodex(
1571
+ ["plugin", "marketplace", "add", packageRoot],
1572
+ "hellolynk_npm_codex_marketplace_add_failed",
1573
+ locations.codexHome,
1574
+ );
1575
+ await ensureExactCodexPluginConfiguration(context);
1576
+ return Object.freeze({ marketplace, plugin: "hellolynk-codex", installed: true });
1577
+ }
1578
+
1579
+ async function ensureExactCodexPluginConfiguration(context) {
1580
+ // A running Codex can already select this exact package while holding its
1581
+ // cache open. Do not ask the CLI to back up and replace those verified bytes.
1582
+ try { await verifyCodexConfiguration(context); return; }
1583
+ catch { /* An incomplete state still needs the official installation entry. */ }
1584
+ let installError = null;
1585
+ try {
1586
+ runCodex(
1587
+ ["plugin", "add", "hellolynk-codex@hellolynk-npm-codex", "--json"],
1588
+ "hellolynk_npm_codex_plugin_add_failed",
1589
+ context.locations.codexHome,
1590
+ );
1591
+ } catch (error) { installError = error; }
1592
+ // CLI failure can follow a completed selection/cache update. Only the full
1593
+ // configuration, source, selected version and exact cache tree prove success;
1594
+ // a partial result retains the original error and normal recovery behavior.
1595
+ try { await verifyCodexConfiguration(context); }
1596
+ catch (error) { throw installError || error; }
1597
+ }
1598
+
1599
+ async function inspectTrustedCodexInstallation({ packageRoot, currentPackageRoot, locations }) {
1600
+ const code = "hellolynk_npm_codex_previous_install_invalid";
1601
+ const releasesRoot = path.dirname(currentPackageRoot);
1602
+ const stableLocations = Object.freeze({ ...locations, finalCurrentRoot: path.dirname(releasesRoot) });
1603
+ if (path.basename(releasesRoot).toLowerCase() !== "releases"
1604
+ || !samePath(path.dirname(packageRoot), releasesRoot)) fail(code);
1605
+ const relative = path.relative(releasesRoot, packageRoot);
1606
+ const match = /^codex-([a-f0-9]{64})$/u.exec(relative);
1607
+ if (!match || path.dirname(relative) !== "." || path.resolve(packageRoot) !== packageRoot) fail(code);
1608
+ await secureStableReleaseTree(stableLocations, path.basename(packageRoot), { verifyOnly: true }).catch(() => fail(code));
1609
+ const previousPackageJson = await readJson(path.join(packageRoot, "package.json"), code, true);
1610
+ const previousMetadata = previousPackageJson?.hellolynkClient;
1611
+ if (previousPackageJson?.name !== "@hellolynk-ai/codex-plugin"
1612
+ || !/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9._-]+)?$/u.test(String(previousPackageJson.version || ""))
1613
+ || !isTrustedCodexMetadata(previousMetadata)) fail(code);
1614
+ const closure = await verifyPackageClosure(packageRoot, match[1], {
1615
+ packageJson: previousPackageJson,
1616
+ metadata: previousMetadata,
1617
+ }).catch(() => fail(code));
1618
+ const marketplaceRecord = await readJson(
1619
+ path.join(packageRoot, ".agents", "plugins", "marketplace.json"), code, true,
1620
+ );
1621
+ const plugins = Array.isArray(marketplaceRecord?.plugins) ? marketplaceRecord.plugins : [];
1622
+ if (marketplaceRecord?.name !== "hellolynk-npm-codex" || plugins.length !== 1
1623
+ || plugins[0]?.name !== "hellolynk-codex"
1624
+ || plugins[0]?.source?.source !== "local"
1625
+ || plugins[0]?.source?.path !== "./plugins/hellolynk-codex") fail(code);
1626
+ const releaseRoot = path.join(packageRoot, ...previousMetadata.releaseRoot.split("/"));
1627
+ const release = await verifyRelease(releaseRoot, packageRoot, previousMetadata).catch(() => fail(code));
1628
+ const pluginManifest = await readJson(
1629
+ path.join(releaseRoot, ".codex-plugin", "plugin.json"), code, true,
1630
+ );
1631
+ if (pluginManifest?.name !== "hellolynk-codex"
1632
+ || !/^[A-Za-z0-9][A-Za-z0-9.+_-]{0,127}$/u.test(String(pluginManifest.version || ""))) fail(code);
1633
+ const activation = await inspectActivation(locations.hostRoot, previousMetadata.profileId);
1634
+ if (!activation.ready) fail(code);
1635
+ const previous = Object.freeze({
1636
+ packageRoot,
1637
+ releaseRoot,
1638
+ release,
1639
+ locations,
1640
+ packageClosure: closure,
1641
+ packageJson: previousPackageJson,
1642
+ metadata: previousMetadata,
1643
+ activation,
1644
+ });
1645
+ const binding = await verifyExactBinding({ ...previous, locations, activation }).catch(() => fail(code));
1646
+ await verifyCodexConfiguration({ ...previous, locations }).catch(() => fail(code));
1647
+ return Object.freeze({ ...previous, expectedOwner: bindingOwner(binding) });
1648
+ }
1649
+
1650
+ async function inspectTrustedPackageRoot({ packageRoot, currentPackageRoot, locations, activation }) {
1651
+ const code = "hellolynk_npm_previous_install_invalid";
1652
+ const releasesRoot = path.dirname(currentPackageRoot);
1653
+ const stableLocations = Object.freeze({ ...locations, finalCurrentRoot: path.dirname(releasesRoot) });
1654
+ if (path.basename(releasesRoot).toLowerCase() !== "releases"
1655
+ || !samePath(path.dirname(packageRoot), releasesRoot)) fail(code);
1656
+ const relative = path.relative(releasesRoot, packageRoot);
1657
+ const match = new RegExp(`^${metadata.client}-([a-f0-9]{64})$`, "u").exec(relative);
1658
+ if (!match || path.dirname(relative) !== "." || path.resolve(packageRoot) !== packageRoot) fail(code);
1659
+ await secureStableReleaseTree(stableLocations, path.basename(packageRoot), { verifyOnly: true }).catch(() => fail(code));
1660
+ const previousPackageJson = await readJson(path.join(packageRoot, "package.json"), code, true);
1661
+ const previousMetadata = previousPackageJson?.hellolynkClient;
1662
+ // TRAE's merged client uses the same global Skill source and destination.
1663
+ // Accept the installed SOLO target name only; every other package field and
1664
+ // the pinned payload/configuration must still match and verify below.
1665
+ const comparableMetadata = metadata.client === "trae-work" && metadata.skillTarget === "trae"
1666
+ && previousMetadata?.skillTarget === "trae-solo"
1667
+ ? { ...previousMetadata, skillTarget: "trae" } : previousMetadata;
1668
+ if (previousPackageJson?.name !== packageJson.name
1669
+ || !/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9._-]+)?$/u.test(String(previousPackageJson.version || ""))
1670
+ || stableJson(comparableMetadata) !== stableJson(metadata)) fail(code);
1671
+ const closure = await verifyPackageClosure(packageRoot, match[1], {
1672
+ packageJson: previousPackageJson,
1673
+ metadata: previousMetadata,
1674
+ // The authenticated installed package may predate Desktop Bridge setup.
1675
+ // Its own pinned manifest/tree still has to verify in full; new packages
1676
+ // continue to require the bridge helper above.
1677
+ installedPreviousRelease: true,
1678
+ }).catch(() => fail(code));
1679
+ const releaseRoot = path.resolve(packageRoot, previousMetadata.releaseRoot);
1680
+ if (!isSameOrDescendant(releaseRoot, packageRoot)) fail(code);
1681
+ const release = await verifyRelease(releaseRoot, packageRoot, previousMetadata).catch(() => fail(code));
1682
+ return Object.freeze({
1683
+ packageRoot,
1684
+ releaseRoot,
1685
+ release,
1686
+ locations,
1687
+ activation,
1688
+ packageClosure: closure,
1689
+ packageJson: previousPackageJson,
1690
+ metadata: previousMetadata,
1691
+ installedPreviousRelease: true,
1692
+ });
1693
+ }
1694
+
1695
+ async function inspectPreviousOrAbsent(current) {
1696
+ return inspectTrustedCurrentBindingPackage(current).catch((error) => {
1697
+ // A missing binding can be initialized. Invalid package/configuration
1698
+ // cannot silently discard the previous recovery point.
1699
+ if (new Set([`neutral_${metadata.client.replaceAll("-", "_")}_binding_configuration_required`,
1700
+ `neutral_${metadata.client.replaceAll("-", "_")}_capability_missing`]).has(error?.code || error?.message)) return null;
1701
+ throw error;
1702
+ });
1703
+ }
1704
+
1705
+ async function inspectTrustedCurrentBindingPackage(current) {
1706
+ const binding = await resolveProtectedClientBinding(current);
1707
+ const releaseRoot = path.resolve(String(binding?.releaseRoot || ""));
1708
+ if (samePath(releaseRoot, current.releaseRoot)) {
1709
+ // Reinstalling this exact package may repair its client configuration.
1710
+ // There is no different previous package to restore in that case.
1711
+ await verifyExactBinding(current);
1712
+ return current;
1713
+ }
1714
+ let previousPackageRoot;
1715
+ if (metadata.releaseRoot === ".") {
1716
+ previousPackageRoot = releaseRoot;
1717
+ } else {
1718
+ const suffix = metadata.releaseRoot.split("/");
1719
+ previousPackageRoot = releaseRoot;
1720
+ for (let index = 0; index < suffix.length; index += 1) previousPackageRoot = path.dirname(previousPackageRoot);
1721
+ if (!samePath(path.resolve(previousPackageRoot, metadata.releaseRoot), releaseRoot)) {
1722
+ fail("hellolynk_npm_previous_install_invalid");
1723
+ }
1724
+ }
1725
+ const previous = await inspectTrustedPackageRoot({
1726
+ packageRoot: previousPackageRoot,
1727
+ currentPackageRoot: current.packageRoot,
1728
+ locations: current.locations,
1729
+ activation: current.activation,
1730
+ });
1731
+ const previousBinding = await verifyExactBinding(previous).catch(() => fail("hellolynk_npm_previous_install_invalid"));
1732
+ // A stale optional listener is repaired by setup, not a reason to block
1733
+ // upgrading otherwise intact account, package and MCP configuration.
1734
+ await verifyClientConfiguration(previous, { verifyNativeService: false }).catch(() => fail("hellolynk_npm_previous_install_invalid"));
1735
+ return Object.freeze({ ...previous, expectedOwner: bindingOwner(previousBinding) });
1736
+ }
1737
+
1738
+ async function resolveProtectedClientBinding({ packageRoot, locations }) {
1739
+ const installationModule = await importStableCore(
1740
+ PACKAGE_ROOT,
1741
+ "hellolynk-neutral-host-installation-store.mjs",
1742
+ "hellolynk_npm_binding_invalid",
1743
+ );
1744
+ const protectedModule = await importStableCore(
1745
+ PACKAGE_ROOT,
1746
+ "hellolynk-host-protected-store.mjs",
1747
+ "hellolynk_npm_binding_invalid",
1748
+ );
1749
+ const verifyPath = ({ path: target, directory }) => protectedModule.verifyHostProtectedStorePath({
1750
+ root: locations.hostRoot,
1751
+ path: target,
1752
+ directory,
1753
+ });
1754
+ const installationStore = installationModule.createNeutralHostInstallationStore({
1755
+ root: locations.hostRoot,
1756
+ readOnly: true,
1757
+ verifyPath,
1758
+ });
1759
+ const store = protectedModule.createHostProtectedStore({
1760
+ root: locations.hostRoot,
1761
+ installationStore,
1762
+ readOnly: true,
1763
+ });
1764
+ return metadata.client === "codex"
1765
+ ? store.verifyCodexNeutralBinding()
1766
+ : store[{
1767
+ openclaw: "verifyOpenClawNeutralBinding",
1768
+ "trae-work": "verifyTraeWorkNeutralBinding",
1769
+ workbuddy: "verifyWorkBuddyNeutralBinding",
1770
+ cursor: "verifyCursorNeutralBinding",
1771
+ dsh: "verifyDshNeutralBinding",
1772
+ }[metadata.client]]({ profileId: metadata.profileId });
1773
+ }
1774
+
1775
+ function stableJson(value) {
1776
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1777
+ if (value && typeof value === "object") {
1778
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
1779
+ }
1780
+ return JSON.stringify(value);
1781
+ }
1782
+
1783
+ function isTrustedCodexMetadata(value) {
1784
+ return value && typeof value === "object" && !Array.isArray(value)
1785
+ && Object.keys(value).sort().join(",") === "activationTool,client,coreRoot,profileId,releaseRoot,serviceBaseUrl"
1786
+ && value.client === "codex"
1787
+ && value.activationTool === "tools/hellolynk-activate-neutral-host-clients.mjs"
1788
+ && value.coreRoot === "plugins/hellolynk-codex/runtime/core"
1789
+ && value.profileId === "hellolynk-host-codex-v2"
1790
+ && value.releaseRoot === "plugins/hellolynk-codex"
1791
+ && value.serviceBaseUrl === "https://hellolynk.com";
1792
+ }
1793
+
1794
+ async function upgradeCodexConfigurationAndBinding(current, prepared) {
1795
+ const previous = prepared?.previous;
1796
+ if (prepared?.kind !== "codex" || prepared.upgrade !== true
1797
+ || !previous || samePath(previous.packageRoot, current.packageRoot)) {
1798
+ fail("hellolynk_npm_codex_upgrade_transaction_required");
1799
+ }
1800
+ const allowedRoots = Object.freeze([previous.packageRoot, current.packageRoot]);
1801
+ let originalError = null;
1802
+ try {
1803
+ await switchCodexConfiguration(current, allowedRoots);
1804
+ } catch (error) {
1805
+ originalError = error;
1806
+ const recovered = await reconcileCodexUpgrade({ current, previous, allowedRoots, prefer: "previous" });
1807
+ if (recovered === "current") return codexConfiguredResult();
1808
+ if (recovered === "previous") {
1809
+ mutationRecoveryState = "none";
1810
+ throw originalError;
1811
+ }
1812
+ throw codexUpgradeOutcomeUncertain(originalError);
1813
+ }
1814
+
1815
+ mutationRecoveryState = "protected_binding_update_attempted";
1816
+ try {
1817
+ await activateBinding(current);
1818
+ await verifyExactBinding(current);
1819
+ await verifyCodexConfiguration(current);
1820
+ mutationRecoveryState = "binding_committed_roll_forward_required";
1821
+ return codexConfiguredResult();
1822
+ } catch (error) {
1823
+ originalError = error;
1824
+ const recovered = await reconcileCodexUpgrade({ current, previous, allowedRoots, prefer: "previous" });
1825
+ if (recovered === "current") {
1826
+ mutationRecoveryState = "binding_committed_roll_forward_required";
1827
+ return codexConfiguredResult();
1828
+ }
1829
+ if (recovered === "previous") {
1830
+ mutationRecoveryState = "none";
1831
+ throw originalError;
1832
+ }
1833
+ throw codexUpgradeOutcomeUncertain(originalError);
1834
+ }
1835
+ }
1836
+
1837
+ async function upgradeClientConfigurationAndBinding(current, previous, preparedConfiguration) {
1838
+ let originalError;
1839
+ mutationRecoveryState = "protected_binding_update_attempted";
1840
+ try {
1841
+ await activateBinding(current);
1842
+ mutationRecoveryState = "binding_committed_roll_forward_required";
1843
+ const configured = await configureClient(current, preparedConfiguration);
1844
+ await verifyExactBinding(current);
1845
+ await verifyClientConfiguration(current);
1846
+ return configured;
1847
+ } catch (error) {
1848
+ originalError = error;
1849
+ }
1850
+ const recovered = await reconcileClientUpgrade({ current, previous, preparedConfiguration, prefer: "previous" });
1851
+ if (recovered.state === "current") {
1852
+ mutationRecoveryState = "binding_committed_roll_forward_required";
1853
+ return recovered.configured;
1854
+ }
1855
+ if (recovered.state === "previous") {
1856
+ mutationRecoveryState = "none";
1857
+ throw originalError;
1858
+ }
1859
+ throw codexUpgradeOutcomeUncertain(originalError);
1860
+ }
1861
+
1862
+ async function reconcileClientUpgrade({ current, previous, preparedConfiguration, prefer }) {
1863
+ const first = prefer === "current" ? current : previous;
1864
+ const second = prefer === "current" ? previous : current;
1865
+ for (const target of [first, second]) {
1866
+ try {
1867
+ const recoveryTarget = compatibleRecoveryTarget(target, previous);
1868
+ if (recoveryTarget.recoveryPrevious) await activateBinding({ ...recoveryTarget, recoveryCheckOnly: true });
1869
+ const configured = await establishClientState(recoveryTarget, preparedConfiguration, target === current ? previous : current);
1870
+ return Object.freeze({
1871
+ state: target === current ? "current" : "previous",
1872
+ configured,
1873
+ });
1874
+ } catch { /* try the other complete, verified state */ }
1875
+ }
1876
+ return Object.freeze({ state: "uncertain", configured: null });
1877
+ }
1878
+
1879
+ function compatibleRecoveryTarget(target, previous) {
1880
+ if (!["codex", "dsh"].includes(metadata.client)) return target;
1881
+ return { ...target, packageClosure: target.packageClosure || candidateClosure,
1882
+ expectedOwner: previous.expectedOwner, recoveryPrevious: previous };
1883
+ }
1884
+
1885
+ async function establishClientState(target, originalConfiguration, otherPackage) {
1886
+ if (isNativeClient()) {
1887
+ const prepared = await preflightClientConfiguration({ ...target, previousCursorPackage: otherPackage });
1888
+ await activateBinding(target);
1889
+ const configured = await configureClient(target, prepared);
1890
+ await verifyExactBinding(target);
1891
+ await verifyClientConfiguration(target);
1892
+ return configured;
1893
+ }
1894
+ const prepared = await preflightClientConfiguration(target);
1895
+ // Recovery may switch only between this transaction's original and proposed
1896
+ // bytes. A fresh preflight is not authorization to overwrite a later edit.
1897
+ const originalPlans = originalConfiguration.kind === "mcp"
1898
+ ? originalConfiguration.configs.map((item) => item.plan) : [originalConfiguration.plan];
1899
+ const recoveryPlans = prepared.kind === "mcp"
1900
+ ? prepared.configs.map((item) => item.plan) : [prepared.plan];
1901
+ if (recoveryPlans.length !== originalPlans.length) fail("hellolynk_npm_configuration_changed_since_preflight");
1902
+ for (const plan of recoveryPlans) {
1903
+ const original = originalPlans.find((item) => samePath(item.configPath, plan.configPath));
1904
+ if (!original || ![original.expectedConfigSha256, original.replacementSha256].includes(plan.expectedConfigSha256)) {
1905
+ fail("hellolynk_npm_configuration_changed_since_preflight");
1906
+ }
1907
+ }
1908
+ const configured = await configureClient(target, prepared);
1909
+ await activateBinding(target);
1910
+ await verifyExactBinding(target);
1911
+ await verifyClientConfiguration(target);
1912
+ return configured;
1913
+ }
1914
+
1915
+ async function reconcileCodexUpgrade({ current, previous, allowedRoots, prefer }) {
1916
+ const first = prefer === "current" ? current : previous;
1917
+ const second = prefer === "current" ? previous : current;
1918
+ for (const target of [first, second]) {
1919
+ try {
1920
+ const recoveryTarget = compatibleRecoveryTarget(target, previous);
1921
+ await activateBinding({ ...recoveryTarget, recoveryCheckOnly: true });
1922
+ await switchCodexConfiguration(recoveryTarget, allowedRoots);
1923
+ await activateBinding(recoveryTarget);
1924
+ await verifyExactBinding(recoveryTarget);
1925
+ await verifyCodexConfiguration(recoveryTarget);
1926
+ return target === current ? "current" : "previous";
1927
+ } catch { /* try the other fully verified state */ }
1928
+ }
1929
+ return "uncertain";
1930
+ }
1931
+
1932
+ async function switchCodexConfiguration(target, allowedRoots) {
1933
+ const marketplace = "hellolynk-npm-codex";
1934
+ const state = codexMarketplaceRoot(target.locations, marketplace);
1935
+ if (state !== null && !allowedRoots.some((root) => samePath(root, state))) {
1936
+ fail("hellolynk_npm_configuration_changed_since_preflight");
1937
+ }
1938
+ if (state !== null && !samePath(state, target.packageRoot)) {
1939
+ tryRunCodex(
1940
+ ["plugin", "remove", `hellolynk-codex@${marketplace}`, "--json"],
1941
+ target.locations.codexHome,
1942
+ );
1943
+ runCodex(
1944
+ ["plugin", "marketplace", "remove", marketplace, "--json"],
1945
+ "hellolynk_npm_codex_marketplace_remove_failed",
1946
+ target.locations.codexHome,
1947
+ );
1948
+ }
1949
+ const afterRemoval = codexMarketplaceRoot(target.locations, marketplace);
1950
+ if (afterRemoval !== null && !samePath(afterRemoval, target.packageRoot)) {
1951
+ fail("hellolynk_npm_codex_marketplace_remove_failed");
1952
+ }
1953
+ if (afterRemoval === null) runCodex(
1954
+ ["plugin", "marketplace", "add", target.packageRoot, "--json"],
1955
+ "hellolynk_npm_codex_marketplace_add_failed",
1956
+ target.locations.codexHome,
1957
+ );
1958
+ await ensureExactCodexPluginConfiguration(target);
1959
+ }
1960
+
1961
+ function codexMarketplaceRoot(locations, marketplace) {
1962
+ const listed = runCodexJson(
1963
+ ["plugin", "marketplace", "list", "--json"],
1964
+ "hellolynk_npm_codex_marketplace_list_failed",
1965
+ locations.codexHome,
1966
+ );
1967
+ if (!Array.isArray(listed?.marketplaces)) fail("hellolynk_npm_codex_marketplace_list_invalid");
1968
+ const matches = listed.marketplaces.filter((item) => item?.name === marketplace);
1969
+ if (matches.length > 1) fail("hellolynk_npm_codex_marketplace_list_invalid");
1970
+ if (matches.length === 0) return null;
1971
+ const root = String(matches[0]?.root || "");
1972
+ if (!path.isAbsolute(root)) fail("hellolynk_npm_codex_marketplace_list_invalid");
1973
+ return path.resolve(root);
1974
+ }
1975
+
1976
+ function tryRunCodex(args, codexHome) {
1977
+ try { runCodex(args, "hellolynk_npm_codex_cleanup_failed", codexHome); }
1978
+ catch { /* marketplace removal is authoritative and may repair a partial plugin state */ }
1979
+ }
1980
+
1981
+ function codexConfiguredResult() {
1982
+ return Object.freeze({
1983
+ marketplace: "hellolynk-npm-codex",
1984
+ plugin: "hellolynk-codex",
1985
+ installed: true,
1986
+ upgraded: true,
1987
+ });
1988
+ }
1989
+
1990
+ function codexUpgradeOutcomeUncertain(cause) {
1991
+ const error = new Error("local_configuration_outcome_uncertain", { cause });
1992
+ error.code = "local_configuration_outcome_uncertain";
1993
+ error.localConfigurationOutcomeUncertain = true;
1994
+ return error;
1995
+ }
1996
+
1997
+ async function preflightOpenClaw({ packageRoot, releaseRoot, locations, allowUnconnected = false }) {
1998
+ const configPath = path.join(locations.home, ".openclaw", "openclaw.json");
1999
+ await readRegularFile(configPath, "hellolynk_npm_openclaw_config_missing");
2000
+ const module = await importLocal(PACKAGE_ROOT, metadata.configurator, "hellolynk_npm_configurator_invalid");
2001
+ const options = { configPath, pluginPath: path.join(packageRoot, metadata.pluginPath), storeRoot: locations.hostRoot, allowUnconnected };
2002
+ const plan = await module.configureOpenClaw({ ...options, apply: false });
2003
+ return Object.freeze({ kind: "openclaw", module, options: Object.freeze(options), plan });
2004
+ }
2005
+
2006
+ async function configureOpenClaw({ packageRoot, releaseRoot, locations }, prepared) {
2007
+ if (prepared?.kind !== "openclaw") fail("hellolynk_npm_configuration_preflight_required");
2008
+ const transaction = await prepared.module.runAgentConfigurationTransaction({
2009
+ plans: [prepared.plan],
2010
+ apply: () => prepared.module.configureOpenClaw({
2011
+ ...prepared.options,
2012
+ expectedConfigPath: prepared.plan.configPath,
2013
+ expectedConfigSha256: prepared.plan.expectedConfigSha256,
2014
+ expectedReplacementSha256: prepared.plan.replacementSha256,
2015
+ }),
2016
+ rollback: (item) => prepared.module.rollbackAgentConfiguration(item),
2017
+ });
2018
+ const [configured] = transaction.committed;
2019
+ return Object.freeze({ configPaths: [configured.configPath], pluginPath: configured.pluginPath });
2020
+ }
2021
+
2022
+ async function preflightMcpClient({ packageRoot, releaseRoot, release, locations }) {
2023
+ // Use this verified installer's transaction and path checks even when
2024
+ // restoring an older pinned release. Only the target payload comes from it.
2025
+ const module = await importLocal(PACKAGE_ROOT, metadata.configurator, "hellolynk_npm_configurator_invalid");
2026
+ const entry = path.join(packageRoot, metadata.entry);
2027
+ await readRegularFile(entry, "hellolynk_npm_entry_invalid");
2028
+ const configPaths = metadata.client === "trae-work"
2029
+ ? await traeConfigPaths(locations.appData)
2030
+ : await workBuddyConfigPaths(locations.home);
2031
+ const entryArgs = metadata.client === "trae-work"
2032
+ ? ["run", "--expected-manifest-sha256", release.manifestSha256]
2033
+ : ["run", "--root", releaseRoot, "--expected-manifest-sha256", release.outerManifestSha256];
2034
+ const configs = [];
2035
+ for (const configPath of configPaths) {
2036
+ const options = Object.freeze({
2037
+ client: metadata.client,
2038
+ configPath,
2039
+ nodePath: process.execPath,
2040
+ proxyPath: entry,
2041
+ entryArgs,
2042
+ storeRoot: locations.hostRoot,
2043
+ allowMissing: true,
2044
+ });
2045
+ const plan = await module.configureNeutralHostMcpClient({ ...options, apply: false });
2046
+ configs.push(Object.freeze({ options, plan }));
2047
+ }
2048
+ // Only the first managed installation opts in. A later upgrade/repair must
2049
+ // not turn a receiver back on after the user has disabled or removed it.
2050
+ const firstInstall = process.platform === "win32" && configs.every(item => item.plan.hadManagedEntry === false);
2051
+ let traeStartup = null, nativeSetupError = null;
2052
+ if (metadata.client === "trae-work" && firstInstall) {
2053
+ try {
2054
+ const worker = await importLocal(PACKAGE_ROOT, "integrations/trae-work/server/native-task-worker.mjs", "hellolynk_npm_trae_receiver_invalid");
2055
+ const currentReceiver = await worker.readTraeNativeTaskWorkerConfig(locations.hostRoot);
2056
+ const options = { configPath: path.join(locations.home, ".trae-cn", "argv.json"),
2057
+ ...(currentReceiver ? { debugEndpoint: currentReceiver } : {}) };
2058
+ const plan = await module.configureTraeNativeStartup({ ...options, apply: false });
2059
+ traeStartup = Object.freeze({ options: { ...options, debugEndpoint: plan.debugEndpoint }, plan, worker, currentReceiver });
2060
+ } catch (error) { nativeSetupError = receptionSetupFailure(error); }
2061
+ }
2062
+ let skill = null;
2063
+ if (metadata.skillTarget) {
2064
+ const skillModule = await importLocal(PACKAGE_ROOT, "tools/hellolynk-install-client-skills.mjs", "hellolynk_npm_skill_installer_invalid");
2065
+ const options = Object.freeze({
2066
+ sourceRoot: packageRoot,
2067
+ home: locations.home,
2068
+ appData: locations.appData,
2069
+ localAppData: locations.localAppData,
2070
+ target: metadata.skillTarget,
2071
+ replace: true,
2072
+ });
2073
+ const plan = await skillModule.installClientSkills({ ...options, apply: false });
2074
+ if (!/^[a-f0-9]{64}$/.test(String(plan?.fingerprint || ""))) {
2075
+ fail("hellolynk_npm_skill_installer_invalid");
2076
+ }
2077
+ skill = Object.freeze({ module: skillModule, options, fingerprint: plan.fingerprint });
2078
+ }
2079
+ return Object.freeze({ kind: "mcp", module, configs: Object.freeze(configs), skill, firstInstall, traeStartup, nativeSetupError });
2080
+ }
2081
+
2082
+ function receptionSetupFailure(error) {
2083
+ const code = String(error?.code || error?.message || "");
2084
+ return { configured: false, errorCode: /^[a-z][a-z0-9_]{1,100}$/.test(code) ? code : "hellolynk_receiver_setup_failed" };
2085
+ }
2086
+
2087
+ async function configureMcpClient({ packageRoot, releaseRoot, release, locations }, prepared) {
2088
+ if (prepared?.kind !== "mcp" || !Array.isArray(prepared.configs)) {
2089
+ fail("hellolynk_npm_configuration_preflight_required");
2090
+ }
2091
+
2092
+ // Revalidate every config and the Skill tree before the first write. This is
2093
+ // what prevents WorkBuddy's second config from being discovered after the
2094
+ // first config has already migrated.
2095
+ for (const item of prepared.configs) {
2096
+ const current = await prepared.module.configureNeutralHostMcpClient({ ...item.options, apply: false });
2097
+ if (current.expectedConfigSha256 !== item.plan.expectedConfigSha256
2098
+ || current.replacementSha256 !== item.plan.replacementSha256) {
2099
+ fail("hellolynk_npm_configuration_changed_since_preflight");
2100
+ }
2101
+ }
2102
+ if (prepared.skill) {
2103
+ const currentSkillPlan = await prepared.skill.module.installClientSkills({
2104
+ ...prepared.skill.options,
2105
+ apply: false,
2106
+ });
2107
+ if (currentSkillPlan.fingerprint !== prepared.skill.fingerprint) {
2108
+ fail("hellolynk_npm_skill_changed_since_preflight");
2109
+ }
2110
+ }
2111
+
2112
+ let nativeService = prepared.nativeSetupError, pageService;
2113
+ const transaction = await prepared.module.runAgentConfigurationTransaction({
2114
+ plans: prepared.configs,
2115
+ apply: (item) => prepared.module.configureNeutralHostMcpClient({
2116
+ ...item.options,
2117
+ expectedConfigPath: item.plan.configPath,
2118
+ expectedConfigSha256: item.plan.expectedConfigSha256,
2119
+ expectedReplacementSha256: item.plan.replacementSha256,
2120
+ }),
2121
+ rollback: (item) => prepared.module.rollbackAgentConfiguration(item),
2122
+ finalize: prepared.skill ? () => prepared.skill.module.installClientSkills({
2123
+ ...prepared.skill.options,
2124
+ apply: true,
2125
+ expectedPlanFingerprint: prepared.skill.fingerprint,
2126
+ }) : null,
2127
+ });
2128
+ // Receiving is an auxiliary setup step. Its failure must not undo a working
2129
+ // MCP/Skill installation or mix their versions with an already installed task.
2130
+ if (metadata.client === "workbuddy") {
2131
+ const context = { packageRoot, releaseRoot, release, locations };
2132
+ try { pageService = { ...await configureWorkBuddyPageTask(context, "Install"), configured: true }; }
2133
+ catch (error) { pageService = receptionSetupFailure(error); }
2134
+ try { nativeService = await updateWorkBuddyNativeService(context, "Install", prepared.firstInstall); }
2135
+ catch (error) { nativeService = receptionSetupFailure(error); }
2136
+ } else if (prepared.traeStartup) {
2137
+ const { worker, currentReceiver, options, plan } = prepared.traeStartup;
2138
+ try {
2139
+ if (await worker.readTraeNativeTaskWorkerConfig(locations.hostRoot) !== currentReceiver) {
2140
+ fail("hellolynk_npm_configuration_changed_since_preflight");
2141
+ }
2142
+ await prepared.module.runAgentConfigurationTransaction({
2143
+ plans: [prepared.traeStartup],
2144
+ apply: () => prepared.module.configureTraeNativeStartup({ ...options, expectedConfigPath: plan.configPath,
2145
+ expectedConfigSha256: plan.expectedConfigSha256, expectedReplacementSha256: plan.replacementSha256 }),
2146
+ rollback: item => prepared.module.rollbackAgentConfiguration(item),
2147
+ finalize: async () => {
2148
+ if (await worker.readTraeNativeTaskWorkerConfig(locations.hostRoot) !== currentReceiver) {
2149
+ fail("hellolynk_npm_configuration_changed_since_preflight");
2150
+ }
2151
+ nativeService = currentReceiver ? { enabled: true, debugEndpoint: currentReceiver }
2152
+ : await worker.configureTraeNativeTaskWorker({ storeRoot: locations.hostRoot, enabled: true, debugEndpoint: options.debugEndpoint });
2153
+ nativeService = { ...nativeService, configured: true, restartRequired: plan.restartRequired };
2154
+ },
2155
+ });
2156
+ } catch (error) { nativeService = receptionSetupFailure(error); }
2157
+ }
2158
+ const configured = transaction.committed;
2159
+ return Object.freeze({
2160
+ configPaths: configured.map((item) => item.configPath),
2161
+ skillInstalled: prepared.skill !== null,
2162
+ ...(nativeService ? { nativeService } : {}),
2163
+ ...(pageService ? { pageService } : {}),
2164
+ ...(nativeService?.errorCode || pageService?.errorCode ? { instructions: {
2165
+ zh: "插件已安装,自动接收或页面入口尚未设置好。请让 AI 根据安装结果检查;现有设置已保留。",
2166
+ en: "The plugin is installed, but receiving or the page entry needs attention. Ask your AI to check the setup result; existing settings were preserved.",
2167
+ } } : nativeService?.restartRequired ? { instructions: {
2168
+ zh: "自动收信已设置好,请完全退出并重新打开客户端一次。",
2169
+ en: "Automatic receiving is set up. Fully quit and reopen the client once.",
2170
+ } } : {}),
2171
+ });
2172
+ }
2173
+
2174
+ function isLegacyOpenClawPluginPath(value) {
2175
+ const candidate = String(value || "");
2176
+ if (!path.isAbsolute(candidate)) return false;
2177
+ const segments = path.resolve(candidate).split(path.sep);
2178
+ const tail = ["integrations", "openclaw-channel"];
2179
+ if (segments.length < tail.length + 3
2180
+ || !segments.slice(-tail.length).every((segment, index) => segment.toLowerCase() === tail[index])) return false;
2181
+ const releaseIndex = segments.length - tail.length - 1;
2182
+ return /^openclaw-[0-9]+\.[0-9]+\.[0-9]+-[A-Za-z0-9._-]+$/i.test(segments[releaseIndex])
2183
+ && segments[releaseIndex - 1]?.toLowerCase() === "releases"
2184
+ && segments[releaseIndex - 2]?.toLowerCase() === "lynk";
2185
+ }
2186
+
2187
+ async function workBuddyConfigPaths(home) {
2188
+ const root = path.join(home, ".workbuddy");
2189
+ const candidates = [path.join(root, ".mcp.json"), path.join(root, "mcp.json")];
2190
+ const existing = [];
2191
+ for (const candidate of candidates) if (await exists(candidate)) existing.push(candidate);
2192
+ return existing.length ? existing : [candidates[0]];
2193
+ }
2194
+
2195
+ async function importLocal(root, relative, code) {
2196
+ const file = path.join(root, relative);
2197
+ if (!isSameOrDescendant(file, root)) fail(code);
2198
+ await readRegularFile(file, code);
2199
+ return import(pathToFileURL(file).href);
2200
+ }
2201
+
2202
+ function runCodex(args, code, codexHome) {
2203
+ let runner;
2204
+ try { runner = resolveCodexRunner(codexHome); }
2205
+ catch (error) {
2206
+ const existing = String(error?.code || "");
2207
+ if (/^[a-z][a-z0-9_]{2,127}$/u.test(existing)) throw error;
2208
+ fail("hellolynk_npm_codex_runner_resolution_internal_failed");
2209
+ }
2210
+ let result;
2211
+ try {
2212
+ result = spawnSync(runner.command, [...runner.argsPrefix, ...args], {
2213
+ encoding: "utf8",
2214
+ windowsHide: true,
2215
+ shell: false,
2216
+ env: minimalEnv({ codexHome }),
2217
+ });
2218
+ } catch {
2219
+ fail(`${code}_${runner.kind}_spawn_threw`);
2220
+ }
2221
+ if (result.status !== 0) {
2222
+ const nested = codeFromOutput(result.stderr, null);
2223
+ if (nested) fail(nested);
2224
+ const spawnCode = String(result.error?.code || "").toLowerCase();
2225
+ if (/^[a-z0-9_]{2,32}$/u.test(spawnCode)) fail(`${code}_${runner.kind}_spawn_${spawnCode}`);
2226
+ if (Number.isInteger(result.status)) {
2227
+ const statusCode = result.status < 0 ? result.status >>> 0 : result.status;
2228
+ fail(`${code}_${runner.kind}_exit_${statusCode}`);
2229
+ }
2230
+ fail(`${code}_${runner.kind}_spawn_failed`);
2231
+ }
2232
+ return result.stdout;
2233
+ }
2234
+
2235
+ function runCodexJson(args, code, codexHome) {
2236
+ const output = runCodex(args, code, codexHome);
2237
+ try { return JSON.parse(output); }
2238
+ catch { fail(`${code}_${resolvedCodexRunner?.kind || "runner"}_json_invalid`); }
2239
+ }
2240
+
2241
+ function minimalEnv({ codexHome = null } = {}) {
2242
+ return Object.freeze({
2243
+ PATH: process.env.Path || process.env.PATH || "",
2244
+ PATHEXT: process.env.PATHEXT || "",
2245
+ SystemRoot: process.env.SystemRoot || process.env.SYSTEMROOT || "",
2246
+ TEMP: process.env.TEMP || "",
2247
+ TMP: process.env.TMP || "",
2248
+ USERPROFILE: process.env.USERPROFILE || "",
2249
+ LOCALAPPDATA: process.env.LOCALAPPDATA || "",
2250
+ APPDATA: process.env.APPDATA || "",
2251
+ ...(codexHome ? { CODEX_HOME: codexHome } : {}),
2252
+ });
2253
+ }
2254
+
2255
+ function resolveCodexRunner(codexHome) {
2256
+ if (resolvedCodexRunner) return resolvedCodexRunner;
2257
+ const searchPath = String(process.env.Path || process.env.PATH || "");
2258
+ let sawLoaderCandidate = false;
2259
+ let sawNativeCandidate = false;
2260
+ for (const rawEntry of searchPath.split(path.delimiter)) {
2261
+ try {
2262
+ const logical = rawEntry.trim().replace(/^"(.*)"$/, "$1");
2263
+ if (!logical || !path.isAbsolute(logical)) continue;
2264
+ // nvm/fnm and user profile mappings can put a junction on PATH. Resolve
2265
+ // that directory once, then retain the ordinary-file checks below it.
2266
+ const entry = realpathSync(path.resolve(logical));
2267
+ const directoryMetadata = lstatSync(entry);
2268
+ if (!directoryMetadata.isDirectory() || directoryMetadata.isSymbolicLink()) continue;
2269
+ const directoryRealPath = realpathSync(entry);
2270
+ try {
2271
+ sawLoaderCandidate ||= lstatSync(path.join(
2272
+ entry, "node_modules", "@openai", "codex", "bin", "codex.js",
2273
+ )).isFile();
2274
+ } catch { /* absent */ }
2275
+ const loader = resolveRegularDescendantSync(
2276
+ entry,
2277
+ directoryRealPath,
2278
+ ["node_modules", "@openai", "codex", "bin", "codex.js"],
2279
+ );
2280
+ if (loader) {
2281
+ resolvedCodexRunner = Object.freeze({
2282
+ command: process.execPath,
2283
+ argsPrefix: Object.freeze([loader]),
2284
+ kind: "npm_loader",
2285
+ });
2286
+ return resolvedCodexRunner;
2287
+ }
2288
+ try { sawNativeCandidate ||= lstatSync(path.join(entry, "codex.exe")).isFile(); }
2289
+ catch { /* absent */ }
2290
+ const candidate = resolveRegularDescendantSync(entry, directoryRealPath, ["codex.exe"]);
2291
+ if (!candidate) continue;
2292
+ const probe = spawnSync(candidate, ["--version"], {
2293
+ encoding: "utf8",
2294
+ windowsHide: true,
2295
+ shell: false,
2296
+ env: minimalEnv({ codexHome }),
2297
+ });
2298
+ if (probe.status !== 0) continue;
2299
+ resolvedCodexRunner = Object.freeze({
2300
+ command: candidate,
2301
+ argsPrefix: Object.freeze([]),
2302
+ kind: "native",
2303
+ });
2304
+ return resolvedCodexRunner;
2305
+ } catch {
2306
+ // A PATH entry without a safe, executable Codex runner is not authoritative.
2307
+ }
2308
+ }
2309
+ const desktopRunner = resolveCodexDesktopRunner(codexHome);
2310
+ if (desktopRunner) return (resolvedCodexRunner = desktopRunner);
2311
+ if (sawLoaderCandidate) fail("hellolynk_npm_codex_loader_invalid");
2312
+ if (sawNativeCandidate) fail("hellolynk_npm_codex_native_invalid");
2313
+ fail("hellolynk_npm_codex_cli_required");
2314
+ }
2315
+
2316
+ function resolveCodexDesktopRunner(codexHome) {
2317
+ if (process.platform !== "win32") return null;
2318
+ const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT;
2319
+ if (!systemRoot || !path.isAbsolute(systemRoot)) return null;
2320
+ // Ask Windows for this user's installed desktop package, so neither a
2321
+ // separate CLI installation nor a hard-coded app version is necessary.
2322
+ const discovered = spawnSync(path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), [
2323
+ "-NoLogo", "-NoProfile", "-NonInteractive", "-Command",
2324
+ "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); Get-AppxPackage -Name OpenAI.Codex -ErrorAction Stop | Select-Object -ExpandProperty InstallLocation",
2325
+ ], { encoding: "utf8", windowsHide: true, shell: false, timeout: 10000,
2326
+ maxBuffer: 32768, env: minimalEnv({ codexHome }) });
2327
+ if (discovered.status !== 0) return null;
2328
+ for (const value of String(discovered.stdout || "").trim().split(/\r?\n/u)) {
2329
+ try {
2330
+ const location = value.trim();
2331
+ if (!location || !path.isAbsolute(location)) continue;
2332
+ const root = realpathSync(location);
2333
+ const candidate = resolveRegularDescendantSync(root, root, ["app", "resources", "codex.exe"]);
2334
+ if (!candidate) continue;
2335
+ const probe = spawnSync(candidate, ["--version"], { encoding: "utf8", windowsHide: true,
2336
+ shell: false, timeout: 5000, env: minimalEnv({ codexHome }) });
2337
+ if (probe.status === 0) return Object.freeze({ command: candidate,
2338
+ argsPrefix: Object.freeze([]), kind: "native" });
2339
+ } catch { /* Ignore an absent or incomplete desktop installation. */ }
2340
+ }
2341
+ return null;
2342
+ }
2343
+
2344
+ function resolveRegularDescendantSync(anchor, anchorRealPath, segments) {
2345
+ let observed = anchor;
2346
+ for (let index = 0; index < segments.length; index += 1) {
2347
+ observed = path.join(observed, segments[index]);
2348
+ let metadata;
2349
+ try { metadata = lstatSync(observed); }
2350
+ catch (error) {
2351
+ if (error?.code === "ENOENT") return null;
2352
+ throw error;
2353
+ }
2354
+ if (metadata.isSymbolicLink()) return null;
2355
+ const final = index === segments.length - 1;
2356
+ if (final ? !metadata.isFile() : !metadata.isDirectory()) return null;
2357
+ }
2358
+ const canonical = realpathSync(observed);
2359
+ const relative = path.relative(
2360
+ normalizeWindowsNamespacePath(anchorRealPath),
2361
+ normalizeWindowsNamespacePath(canonical),
2362
+ );
2363
+ if (relative.toLowerCase() !== path.join(...segments).toLowerCase()) return null;
2364
+ return canonical;
2365
+ }
2366
+
2367
+ function parseCodexConfiguration(source, code) {
2368
+ if (typeof source !== "string" || source.includes("\0")) fail(code);
2369
+ const tableNames = Object.freeze({
2370
+ "marketplaces.hellolynk-npm-codex": "marketplace",
2371
+ 'plugins."hellolynk-codex@hellolynk-npm-codex"': "plugin",
2372
+ });
2373
+ const tables = { marketplace: null, plugin: null };
2374
+ let current = null;
2375
+ for (const rawLine of source.replace(/\r\n?/gu, "\n").split("\n")) {
2376
+ const line = stripTomlComment(rawLine).trim();
2377
+ if (!line) continue;
2378
+ const table = /^\[([^\]]+)\]$/u.exec(line);
2379
+ if (table) {
2380
+ current = tableNames[table[1]] || null;
2381
+ if (current) {
2382
+ if (tables[current] !== null) fail(code);
2383
+ tables[current] = new Map();
2384
+ }
2385
+ continue;
2386
+ }
2387
+ if (!current) continue;
2388
+ const separator = findTomlAssignment(line);
2389
+ if (separator < 1) fail(code);
2390
+ const key = line.slice(0, separator).trim();
2391
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/u.test(key) || tables[current].has(key)) fail(code);
2392
+ tables[current].set(key, parseTomlScalar(line.slice(separator + 1).trim(), code));
2393
+ }
2394
+ if (!(tables.marketplace instanceof Map) || !(tables.plugin instanceof Map)) fail(code);
2395
+ return Object.freeze({
2396
+ marketplace: Object.freeze({
2397
+ sourceType: tables.marketplace.get("source_type"),
2398
+ source: tables.marketplace.get("source"),
2399
+ }),
2400
+ plugin: Object.freeze({ enabled: tables.plugin.get("enabled") }),
2401
+ });
2402
+ }
2403
+
2404
+ function stripTomlComment(value) {
2405
+ let quote = null;
2406
+ let escaped = false;
2407
+ for (let index = 0; index < value.length; index += 1) {
2408
+ const character = value[index];
2409
+ if (quote === '"') {
2410
+ if (escaped) escaped = false;
2411
+ else if (character === "\\") escaped = true;
2412
+ else if (character === quote) quote = null;
2413
+ } else if (quote === "'") {
2414
+ if (character === quote) quote = null;
2415
+ } else if (character === '"' || character === "'") quote = character;
2416
+ else if (character === "#") return value.slice(0, index);
2417
+ }
2418
+ return value;
2419
+ }
2420
+
2421
+ function findTomlAssignment(value) {
2422
+ let quote = null;
2423
+ let escaped = false;
2424
+ for (let index = 0; index < value.length; index += 1) {
2425
+ const character = value[index];
2426
+ if (quote === '"') {
2427
+ if (escaped) escaped = false;
2428
+ else if (character === "\\") escaped = true;
2429
+ else if (character === quote) quote = null;
2430
+ } else if (quote === "'") {
2431
+ if (character === quote) quote = null;
2432
+ } else if (character === '"' || character === "'") quote = character;
2433
+ else if (character === "=") return index;
2434
+ }
2435
+ return -1;
2436
+ }
2437
+
2438
+ function parseTomlScalar(value, code) {
2439
+ if (value === "true") return true;
2440
+ if (value === "false") return false;
2441
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
2442
+ return value.slice(1, -1);
2443
+ }
2444
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
2445
+ try { return JSON.parse(value); }
2446
+ catch { fail(code); }
2447
+ }
2448
+ fail(code);
2449
+ }
2450
+
2451
+ async function assertCanonicalDirectoryChain(anchor, candidate, code) {
2452
+ const anchorPath = path.resolve(anchor);
2453
+ const candidatePath = path.resolve(candidate);
2454
+ const relative = path.relative(anchorPath, candidatePath);
2455
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) fail(code);
2456
+ const anchorMetadata = await fs.lstat(anchorPath).catch(() => fail(code));
2457
+ if (!anchorMetadata.isDirectory() || anchorMetadata.isSymbolicLink()) fail(code);
2458
+ const anchorCanonical = await fs.realpath(anchorPath).catch(() => fail(code));
2459
+ let observed = anchorPath;
2460
+ let expected = anchorCanonical;
2461
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
2462
+ observed = path.join(observed, segment);
2463
+ expected = path.join(expected, segment);
2464
+ const metadata = await fs.lstat(observed).catch(() => fail(code));
2465
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) fail(code);
2466
+ const canonical = await fs.realpath(observed).catch(() => fail(code));
2467
+ if (!samePath(canonical, expected)) fail(code);
2468
+ }
2469
+ }
2470
+
2471
+ async function verifyExactRegularTree(sourceRoot, targetRoot, trustedTargetAnchor, code) {
2472
+ const source = await snapshotRegularTree(sourceRoot, null, code);
2473
+ const target = await snapshotRegularTree(targetRoot, trustedTargetAnchor, code);
2474
+ if (source.size !== target.size) fail(code);
2475
+ for (const [relative, expected] of source) {
2476
+ const observed = target.get(relative);
2477
+ if (!observed || observed.bytes !== expected.bytes || observed.sha256 !== expected.sha256) fail(code);
2478
+ }
2479
+ }
2480
+
2481
+ async function snapshotRegularTree(root, trustedAnchor, code) {
2482
+ const rootPath = path.resolve(root);
2483
+ if (trustedAnchor) await assertCanonicalDirectoryChain(trustedAnchor, rootPath, code);
2484
+ const rootMetadata = await fs.lstat(rootPath).catch(() => fail(code));
2485
+ if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) fail(code);
2486
+ const rootCanonical = await fs.realpath(rootPath).catch(() => fail(code));
2487
+ const records = new Map();
2488
+ async function visit(directory) {
2489
+ const entries = await fs.readdir(directory).catch(() => fail(code));
2490
+ entries.sort((left, right) => left.localeCompare(right, "en"));
2491
+ for (const name of entries) {
2492
+ const absolute = path.join(directory, name);
2493
+ const relative = path.relative(rootPath, absolute).split(path.sep).join("/");
2494
+ const before = await fs.lstat(absolute).catch(() => fail(code));
2495
+ if (before.isSymbolicLink()) fail(code);
2496
+ if (before.isDirectory()) {
2497
+ const canonical = await fs.realpath(absolute).catch(() => fail(code));
2498
+ if (!samePath(canonical, path.join(rootCanonical, ...relative.split("/")))) fail(code);
2499
+ await visit(absolute);
2500
+ } else if (before.isFile() && before.nlink === 1) {
2501
+ const body = await fs.readFile(absolute).catch(() => fail(code));
2502
+ const after = await fs.lstat(absolute).catch(() => fail(code));
2503
+ if (!sameFileIdentity(before, after) || after.size !== body.length) fail(code);
2504
+ records.set(relative, Object.freeze({ bytes: body.length, sha256: sha256(body) }));
2505
+ } else fail(code);
2506
+ }
2507
+ }
2508
+ await visit(rootPath);
2509
+ return records;
2510
+ }
2511
+
2512
+ function sameFileIdentity(left, right) {
2513
+ return left.isFile() && right.isFile() && !right.isSymbolicLink()
2514
+ && left.dev === right.dev && left.ino === right.ino
2515
+ && left.size === right.size && left.mtimeMs === right.mtimeMs && right.nlink === 1;
2516
+ }
2517
+
2518
+ function samePath(left, right) {
2519
+ const normalize = (value) => {
2520
+ const resolved = normalizeWindowsNamespacePath(value);
2521
+ let canonical = resolved;
2522
+ try { canonical = realpathSync(resolved); } catch { /* comparison can still reject a missing path */ }
2523
+ return normalizeWindowsNamespacePath(canonical).replace(/[\\/]+$/u, "").toLowerCase();
2524
+ };
2525
+ return normalize(left) === normalize(right);
2526
+ }
2527
+
2528
+ function normalizeWindowsNamespacePath(value) {
2529
+ let input = String(value || "");
2530
+ if (/^\\\\\?\\UNC\\/iu.test(input)) input = `\\\\${input.slice(8)}`;
2531
+ else if (/^\\\\\?\\/u.test(input)) input = input.slice(4);
2532
+ return path.resolve(input);
2533
+ }
2534
+
2535
+ function codeFromOutput(value, fallback) {
2536
+ try {
2537
+ const parsed = JSON.parse(String(value || "").trim().split(/\r?\n/).at(-1));
2538
+ return /^[a-z][a-z0-9_]{2,127}$/.test(parsed?.error) ? parsed.error : fallback;
2539
+ } catch { return fallback; }
2540
+ }
2541
+
2542
+ async function readJson(file, code, requireSingleLink = false) {
2543
+ let bytes;
2544
+ try { bytes = await readRegularFile(file, code, requireSingleLink); }
2545
+ catch { fail(code); }
2546
+ if (bytes.length > MAX_JSON_BYTES) fail(code);
2547
+ try { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); }
2548
+ catch { fail(code); }
2549
+ }
2550
+
2551
+ async function readRegularFile(file, code, requireSingleLink = false) {
2552
+ let metadata;
2553
+ try { metadata = await fs.lstat(file); }
2554
+ catch { fail(code); }
2555
+ if (!metadata.isFile() || metadata.isSymbolicLink() || (requireSingleLink && metadata.nlink !== 1)) fail(code);
2556
+ return fs.readFile(file);
2557
+ }
2558
+
2559
+ async function exists(file) { return fs.access(file).then(() => true, () => false); }
2560
+ function sha256(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
2561
+ function fail(code) { const error = new Error(code); error.code = code; throw error; }
2562
+ function safeCode(error) {
2563
+ const value = String(error?.code || error?.message || "hellolynk_npm_setup_failed");
2564
+ return /^[a-z][a-z0-9_]{2,127}$/.test(value) ? value : "hellolynk_npm_setup_failed";
2565
+ }
2566
+ function failureResult(error, recoveryState) {
2567
+ const result = { ok: false, error: safeCode(error) };
2568
+ if (result.error === "hellolynk_native_existing_profile_conflict") {
2569
+ return Object.freeze({ ...result, recoveryState: "existing_profile_preserved", recovery: "inspect_existing_profile" });
2570
+ }
2571
+ if (recoveryState === "device_connect_attempted"
2572
+ && ["device_enrollment_existing_profile_requires_setup", "device_enrollment_profile_conflict"].includes(result.error)) {
2573
+ return Object.freeze({
2574
+ ...result,
2575
+ recoveryState: "existing_profile_preserved",
2576
+ recovery: result.error === "device_enrollment_existing_profile_requires_setup"
2577
+ ? "run_doctor_then_setup_same_candidate" : "inspect_existing_profile",
2578
+ });
2579
+ }
2580
+ if (error?.localConfigurationOutcomeUncertain === true
2581
+ || result.error === "local_configuration_outcome_uncertain") {
2582
+ return Object.freeze({
2583
+ ...result,
2584
+ recoveryState: "local_configuration_outcome_uncertain",
2585
+ localConfigurationRolledBack: "unknown",
2586
+ ...(recoveryState !== "none" ? { bindingRecoveryState: recoveryState } : {}),
2587
+ recovery: recoveryState.startsWith("device_")
2588
+ ? "inspect_local_configuration_then_repeat_connect_same_arguments_same_candidate"
2589
+ : "inspect_local_configuration_then_run_setup_same_candidate",
2590
+ });
2591
+ }
2592
+ if (recoveryState === "device_connect_attempted"
2593
+ || recoveryState === "device_registration_committed_roll_forward_required") {
2594
+ return Object.freeze({
2595
+ ...result,
2596
+ recoveryState: recoveryState === "device_connect_attempted"
2597
+ ? "device_connect_outcome_uncertain" : recoveryState,
2598
+ recovery: "repeat_connect_same_arguments_same_candidate",
2599
+ });
2600
+ }
2601
+ if (recoveryState === "binding_redemption_attempted") {
2602
+ return Object.freeze({
2603
+ ...result,
2604
+ recoveryState: "binding_outcome_uncertain",
2605
+ bindingKeyReusable: "unknown",
2606
+ recovery: "run_doctor_then_setup_same_candidate",
2607
+ });
2608
+ }
2609
+ if (recoveryState === "protected_binding_update_attempted") {
2610
+ return Object.freeze({
2611
+ ...result,
2612
+ recoveryState: "protected_binding_update_outcome_uncertain",
2613
+ recovery: "run_doctor_then_setup_same_candidate",
2614
+ });
2615
+ }
2616
+ if (recoveryState === "binding_committed_roll_forward_required") {
2617
+ return Object.freeze({
2618
+ ...result,
2619
+ recoveryState: "binding_committed_roll_forward_required",
2620
+ recovery: "run_setup_same_candidate",
2621
+ });
2622
+ }
2623
+ return Object.freeze(result);
2624
+ }
2625
+ function print(value) {
2626
+ process.stdout.write(jsonOutput ? `${JSON.stringify(value)}\n` : resultText(value));
2627
+ }
2628
+
2629
+ function say(zh, en) { return CHINESE ? zh : en; }
2630
+
2631
+ function helpText() {
2632
+ const script = path.resolve(process.argv[1]);
2633
+ const relative = path.relative(process.cwd(), script);
2634
+ const shown = relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : script;
2635
+ const run = `node ${/\s/.test(shown) ? `"${shown}"` : shown}`;
2636
+ const connect = `connect --hellolynk-id ${say("<灵信号>", "<HelloLynk ID>")}`;
2637
+ const rows = isNativeClient()
2638
+ ? [[say("连接账号", "Connect an account"), connect], [say("检查", "Check"), "doctor"]]
2639
+ : [[say("安装", "Install"), "init"], [say("连接账号", "Connect an account"), connect],
2640
+ [say("更新或修复", "Update or repair"), "setup"], [say("检查", "Check"), "doctor"],
2641
+ [say("卸载", "Uninstall"), "uninstall"]];
2642
+ return `${[`HelloLynk for ${CLIENT_NAMES[metadata.client]}${say("(Windows,Node.js 22.16.0+)", " (Windows, Node.js 22.16.0+)")}`,
2643
+ ...rows.map(([label, args]) => `${label}${say(":", ": ")}${run} ${args}`),
2644
+ say("加 --json 输出一行 JSON。", "Add --json for one line of JSON.")].join("\n")}\n`;
2645
+ }
2646
+
2647
+ function resultText(value) {
2648
+ const name = CLIENT_NAMES[metadata.client];
2649
+ const reopen = say(`完全退出并重新打开 ${name} 后生效。`, `Fully quit and reopen ${name} for this to take effect.`);
2650
+ const lines = [];
2651
+ if (value.status === "confirmation_required") {
2652
+ lines.push(say("还差一步:打开下面的链接,在官网确认这台电脑,再运行一次同样的命令。",
2653
+ "One more step: open the link below, confirm this computer on the website, then run the same command again."), value.verificationUrl);
2654
+ } else if (value.uninstalled) {
2655
+ lines.push(`${say(`HelloLynk 已从 ${name} 卸载。`, `HelloLynk is uninstalled from ${name}. `)}${reopen}`,
2656
+ say("账号、联系人和聊天记录留在这台电脑上,重新安装后不用重新连接。",
2657
+ "Your account, contacts and chat history stay on this computer; reinstalling needs no new connection."));
2658
+ if (value.releaseCleanup?.complete !== true) {
2659
+ lines.push(say("有的版本还在使用,下次安装或卸载时删除。",
2660
+ "Some versions are still in use; they are removed the next time you install or uninstall."));
2661
+ }
2662
+ } else if (value.releaseVerified) {
2663
+ lines.push(value.clientConfigurationVerified
2664
+ ? say(`HelloLynk ${value.packageVersion} 已安装,账号已连接。`, `HelloLynk ${value.packageVersion} is installed and connected.`)
2665
+ : say(`HelloLynk ${value.packageVersion} 安装包完好,这台电脑还没有连接账号。`,
2666
+ `The HelloLynk ${value.packageVersion} package is intact; this computer is not connected yet.`));
2667
+ } else if (value.next === "open_client_and_connect") {
2668
+ lines.push(say(`HelloLynk 已装进 ${name}。完全退出并重新打开 ${name},说“打开 HelloLynk”,按页面提示连接账号。`,
2669
+ `HelloLynk is installed in ${name}. Fully quit and reopen ${name}, say "open HelloLynk", and connect your account on the page.`));
2670
+ } else if (value.recovered) {
2671
+ lines.push(`${say(`已恢复到 HelloLynk ${value.restoredPackageVersion}。`, `Restored HelloLynk ${value.restoredPackageVersion}. `)}${reopen}`);
2672
+ } else if (value.status === "registered") {
2673
+ lines.push(`${say(`已连接灵信号 ${value.hellolynkId}。`, `Connected to ${value.hellolynkId}. `)}${reopen}`);
2674
+ } else {
2675
+ lines.push(`${value.packageVersion ? say(`HelloLynk ${value.packageVersion} 已就绪。`, `HelloLynk ${value.packageVersion} is ready. `)
2676
+ : say("HelloLynk 已连接。", "HelloLynk is connected. ")}${reopen}`);
2677
+ }
2678
+ const receiving = value.configured?.nativeService?.errorCode, page = value.configured?.pageService?.errorCode;
2679
+ if (receiving) lines.push(say(`自动收信没有设置好(${receiving})。`, `Automatic receiving is not set up (${receiving}).`));
2680
+ if (page) lines.push(say(`HelloLynk 页面入口没有设置好(${page})。`, `The HelloLynk page entry is not set up (${page}).`));
2681
+ const handover = {
2682
+ pending: say("这台电脑上的 HelloLynk 还在运行旧版本。请关闭所有使用 HelloLynk 的客户端,再重新打开。",
2683
+ "HelloLynk on this computer still runs an earlier version. Close every client that uses HelloLynk, then open them again."),
2684
+ unconfirmed: say("无法确认这台电脑上的 HelloLynk 运行的版本。请关闭所有使用 HelloLynk 的客户端,再重新打开。",
2685
+ "Which version HelloLynk on this computer runs could not be checked. Close every client that uses HelloLynk, then open them again."),
2686
+ }[value.hostHandover?.status];
2687
+ if (handover) lines.push(handover);
2688
+ return `${lines.join("\n")}\n`;
2689
+ }
2690
+
2691
+ // The code stays on the first line for support; the second says what happened and what to do.
2692
+ function failureText(failure) {
2693
+ const name = CLIENT_NAMES[metadata.client];
2694
+ const byState = {
2695
+ existing_profile_preserved: say("这台电脑已有连接资料,没有改动。运行 doctor 检查。",
2696
+ "This computer already has connection data; nothing was changed. Run doctor to check it."),
2697
+ local_configuration_outcome_uncertain: say(`${name} 的设置可能只改了一部分。再运行一次这条命令。`,
2698
+ `${name}'s settings may be only partly changed. Run this command again.`),
2699
+ device_connect_outcome_uncertain: say("不确定是否已连接。用同样的参数再运行一次。",
2700
+ "Whether it connected is unknown. Run the same command again."),
2701
+ device_registration_committed_roll_forward_required: say("账号已连接,客户端设置还没写完。用同样的参数再运行一次。",
2702
+ "The account is connected, but the client settings are not finished. Run the same command again."),
2703
+ binding_outcome_uncertain: say("不确定是否已连接。先运行 doctor,再运行 setup。",
2704
+ "Whether it connected is unknown. Run doctor, then setup."),
2705
+ protected_binding_update_outcome_uncertain: say("不确定更新是否完成。先运行 doctor,再运行 setup。",
2706
+ "Whether the update finished is unknown. Run doctor, then setup."),
2707
+ binding_committed_roll_forward_required: say("账号已连接,客户端设置还没写完。运行 setup 完成。",
2708
+ "The account is connected, but the client settings are not finished. Run setup to finish."),
2709
+ }[failure.recoveryState];
2710
+ return `${failure.error}\n${byState || failureReason(failure.error, name)}\n`;
2711
+ }
2712
+
2713
+ function failureReason(code, name) {
2714
+ if (code === "hellolynk_node_22_16_required") return say("需要 Node.js 22.16 或更高版本。", "Node.js 22.16 or later is required.");
2715
+ if (code === "hellolynk_npm_windows_only") return say("只支持 Windows。", "Windows only.");
2716
+ if (code === "hellolynk_npm_connect_id_invalid") {
2717
+ return say("灵信号不对。请从官网复制从 hellolynk: 开始的完整灵信号。",
2718
+ "This HelloLynk ID is not valid. Copy the complete ID, starting with hellolynk:, from the website.");
2719
+ }
2720
+ if (/_(?:command|arguments)_invalid$|^hellolynk_npm_binding_key_stdin_flag_required$/.test(code)) {
2721
+ return say("命令或参数不对。运行 --help 查看用法。", "The command or its arguments are not valid. Run --help to see usage.");
2722
+ }
2723
+ if (/^hellolynk_(?:npm|native)_(?:pack|release|stable_release|package)_invalid$/.test(code)) {
2724
+ return say("安装包不完整或被改动过。请重新下载。", "The package is incomplete or was changed. Download it again.");
2725
+ }
2726
+ if (code === "hellolynk_npm_existing_host_activation_required") {
2727
+ return say(`这台电脑还没有连接账号。先运行 init,再在 ${name} 里连接。`, `This computer is not connected yet. Run init, then connect in ${name}.`);
2728
+ }
2729
+ if (code === "hellolynk_npm_openclaw_config_missing") {
2730
+ return say("没有找到 OpenClaw 的配置文件。先打开一次 OpenClaw,再运行这条命令。",
2731
+ "OpenClaw's configuration file was not found. Open OpenClaw once, then run this command again.");
2732
+ }
2733
+ if (/^hellolynk_npm_codex_(?:cli_required|loader_invalid|native_invalid)$/.test(code)) {
2734
+ return say("找不到可用的 Codex。安装或打开一次 Codex 后再试。", "No usable Codex was found. Install or open Codex once, then try again.");
2735
+ }
2736
+ if (/_changed_since_preflight$|^workbench_account_changed$/.test(code)) {
2737
+ return say("运行期间设置被改动了。再运行一次这条命令。", "Settings changed while this ran. Run this command again.");
2738
+ }
2739
+ if (code === "hellolynk_npm_uninstall_config_conflict") {
2740
+ return say(`${name} 里有一项 HelloLynk 设置不是安装器写入的,没有删除任何东西。删掉这项设置后再运行一次。`,
2741
+ `A HelloLynk setting in ${name} was not written by the installer; nothing was removed. Remove that setting, then run this command again.`);
2742
+ }
2743
+ if (code === "hellolynk_npm_uninstall_skill_conflict") {
2744
+ return say("HelloLynk 技能文件被改动过,没有删除任何东西。还原或删掉这个技能后再运行一次。",
2745
+ "The HelloLynk skill files were changed; nothing was removed. Restore or delete the skill, then run this command again.");
2746
+ }
2747
+ return say("没有完成。处理方法见 README.md。", "This did not complete. See README.md.");
2748
+ }