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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/LICENSE +92 -0
  3. package/README.md +86 -2
  4. package/bin/hellolynk-client-setup.mjs +2748 -0
  5. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  6. package/hellolynk-client-pack-manifest.json +619 -0
  7. package/hellolynk-client-pack-pin.json +4 -0
  8. package/package.json +58 -4
  9. package/plugins/hellolynk-codex/.app.json +3 -0
  10. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
  11. package/plugins/hellolynk-codex/.mcp.json +19 -0
  12. package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
  13. package/plugins/hellolynk-codex/package.json +11 -0
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  15. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  16. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  17. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  18. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  19. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  20. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  21. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  22. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  23. package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  24. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  34. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  35. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  36. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
  37. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  38. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  39. package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
  40. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  41. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  42. package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  43. package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  44. package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  45. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  46. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  47. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  48. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  49. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  50. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  51. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  52. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  53. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
  54. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
  55. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  56. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
  57. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  58. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  59. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
  60. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  61. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  62. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  63. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  64. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  65. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  66. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  67. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  68. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  69. package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  70. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +160 -0
  71. package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  72. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  73. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  74. package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
  75. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
  76. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
  77. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
  78. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
  79. package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
  80. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
  81. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  82. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  83. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  84. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  85. package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
  86. package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
  87. package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
  88. package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
  89. package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
  90. package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
  91. package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
  92. package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
  93. package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
  94. package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
  95. package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
  96. package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
  97. package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
  98. package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
  99. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  100. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
  101. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
  102. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
  103. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
  104. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
  105. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
  106. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
  107. package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
  108. package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
  109. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  110. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  111. package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
  112. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
  113. package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
  114. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
  115. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
  116. package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
  117. package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
  118. package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
  119. package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
  120. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
  121. package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
  122. package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
  123. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { redeemAgentBindingKey, runSanitizedDoctor } from "./hellolynk-client-core.mjs";
6
+ import { createPlatformCredentialStore } from "./hellolynk-platform-credential-store.mjs";
7
+ import {
8
+ defaultHelloLynkClientRoot,
9
+ readClientProfile,
10
+ writeClientProfile,
11
+ } from "./hellolynk-client-profile.mjs";
12
+ import {
13
+ createNeutralHostInstallationStore,
14
+ writeNeutralHostProfileManifest,
15
+ } from "./hellolynk-neutral-host-installation-store.mjs";
16
+ import { createHostProtectedStore } from "./hellolynk-host-protected-store.mjs";
17
+ import { createDeviceEnrollmentClient } from "./hellolynk-device-enrollment.mjs";
18
+
19
+ function parseArgs(argv) {
20
+ const [command, ...rest] = argv;
21
+ const options = {};
22
+ for (let index = 0; index < rest.length; index += 1) {
23
+ const part = rest[index];
24
+ if ([
25
+ "--allow-loopback-http", "--binding-key-from-stdin", "--bootstrap-host-transport",
26
+ ].includes(part)) {
27
+ options[part.slice(2)] = true;
28
+ continue;
29
+ }
30
+ if (!part.startsWith("--") || index + 1 >= rest.length) throw new Error("client_arguments_invalid");
31
+ options[part.slice(2)] = rest[index + 1];
32
+ index += 1;
33
+ }
34
+ return { command, options };
35
+ }
36
+
37
+ function required(options, key) {
38
+ const value = String(options[key] || "").trim();
39
+ if (!value) throw new Error(`client_argument_${key}_required`);
40
+ return value;
41
+ }
42
+
43
+ async function readBindingKeyFromStdin() {
44
+ const chunks = [];
45
+ let bytes = 0;
46
+ for await (const input of process.stdin) {
47
+ const chunk = Buffer.from(input);
48
+ bytes += chunk.length;
49
+ if (bytes > 4 * 1024) throw new Error("binding_key_input_too_large");
50
+ chunks.push(chunk);
51
+ const value = Buffer.concat(chunks);
52
+ const lineEnd = value.indexOf(0x0a);
53
+ if (lineEnd >= 0) {
54
+ const line = value.subarray(0, lineEnd).toString("utf8").replace(/\r$/, "");
55
+ if (!line) throw new Error("binding_key_stdin_required");
56
+ return line;
57
+ }
58
+ }
59
+ const value = Buffer.concat(chunks).toString("utf8").replace(/\r$/, "");
60
+ if (!value) throw new Error("binding_key_stdin_required");
61
+ return value;
62
+ }
63
+
64
+ function publicErrorCode(error) {
65
+ const code = String(error?.code || error?.message || "client_operation_failed");
66
+ return /^[a-z][a-z0-9_]{0,127}$/.test(code) ? code : "client_operation_failed";
67
+ }
68
+
69
+ async function bind(options, dependencies = {}) {
70
+ if (options["binding-key-from-stdin"] !== true) throw new Error("binding_key_stdin_flag_required");
71
+ const root = options.root || defaultHelloLynkClientRoot();
72
+ const profileId = required(options, "profile");
73
+ const configPath = options.config || join(root, "profiles", `${profileId}.json`);
74
+ const bindingKey = await (dependencies.readSecret || readBindingKeyFromStdin)();
75
+ const store = dependencies.store || createPlatformCredentialStore({ storeRoot: join(root, "credentials") });
76
+ const redeemed = await redeemAgentBindingKey({
77
+ baseUrl: required(options, "base-url"),
78
+ bindingKey,
79
+ deviceId: required(options, "device-id"),
80
+ hostInstallationId: required(options, "host-installation-id"),
81
+ bootstrapHostTransport: options["bootstrap-host-transport"] === true,
82
+ allowLoopbackHttp: options["allow-loopback-http"] === true,
83
+ fetchImpl: dependencies.fetchImpl,
84
+ });
85
+ await store.save(profileId, redeemed.credential);
86
+ try {
87
+ const profile = await writeClientProfile(configPath, {
88
+ contract: "hellolynk-client-profile-v1",
89
+ schemaVersion: redeemed.hostId && redeemed.authorizationVersion ? 2 : 1,
90
+ profileId,
91
+ baseUrl: options["base-url"],
92
+ allowLoopbackHttp: options["allow-loopback-http"] === true,
93
+ deviceId: redeemed.deviceId,
94
+ credentialId: redeemed.credentialId,
95
+ principalId: redeemed.principalId,
96
+ hellolynkId: redeemed.hellolynkId,
97
+ agentId: redeemed.agentId,
98
+ delegateId: redeemed.delegateId,
99
+ ...(redeemed.hostId && redeemed.authorizationVersion ? {
100
+ hostId: redeemed.hostId,
101
+ authorizationVersion: redeemed.authorizationVersion,
102
+ } : {}),
103
+ region: redeemed.region,
104
+ allowedScopes: redeemed.allowedScopes,
105
+ platform: process.platform,
106
+ });
107
+ const neutralHostRoot = options["neutral-host-root"] == null
108
+ ? null : resolve(required(options, "neutral-host-root"));
109
+ let neutralHostActivated = false;
110
+ let bootstrapTransportStored = false;
111
+ if (neutralHostRoot) {
112
+ if (options.adapter == null) throw new Error("client_argument_adapter_required");
113
+ const installationStore = dependencies.installationStore
114
+ || createNeutralHostInstallationStore({ root: neutralHostRoot });
115
+ const installation = await installationStore.adoptProfileFile({
116
+ profileFile: configPath,
117
+ adapterId: required(options, "adapter"),
118
+ });
119
+ await writeActivationProfileManifest({
120
+ clientRoot: root,
121
+ configPath,
122
+ neutralHostRoot,
123
+ installation,
124
+ });
125
+ if (redeemed.bootstrapHostTransportCredential) {
126
+ const protectedStore = dependencies.protectedStore || createHostProtectedStore({
127
+ root: neutralHostRoot,
128
+ installationStore,
129
+ });
130
+ await protectedStore.saveTransportCredential({
131
+ credentialId: redeemed.bootstrapHostTransportCredential.credentialId,
132
+ principalId: redeemed.principalId,
133
+ hostInstallationId: redeemed.hostId,
134
+ state: "active",
135
+ credential: redeemed.bootstrapHostTransportCredential.credential,
136
+ createdAt: redeemed.bootstrapHostTransportCredential.createdAt,
137
+ expiresAt: redeemed.bootstrapHostTransportCredential.expiresAt,
138
+ revokedAt: null,
139
+ });
140
+ bootstrapTransportStored = true;
141
+ }
142
+ neutralHostActivated = true;
143
+ }
144
+ return {
145
+ ok: true,
146
+ profileId: profile.profileId,
147
+ credentialStored: true,
148
+ configPath,
149
+ neutralHostActivated,
150
+ bootstrapTransportStored,
151
+ };
152
+ } catch (error) {
153
+ await store.remove(profileId).catch(() => {});
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ async function writeActivationProfileManifest(input) {
159
+ const manifestPath = join(input.neutralHostRoot, "profile-manifest-v1.json");
160
+ const knownPaths = new Map([[basename(input.configPath, ".json"), input.configPath]]);
161
+ try {
162
+ const existing = JSON.parse(await readFile(manifestPath, "utf8"));
163
+ if (existing?.contract === "hellolynk-neutral-host-profile-manifest-v1" && Array.isArray(existing.profiles)) {
164
+ for (const candidate of existing.profiles) {
165
+ const absolute = resolve(String(candidate || ""));
166
+ knownPaths.set(basename(absolute, ".json"), absolute);
167
+ }
168
+ }
169
+ } catch {}
170
+ const profileIds = input.installation.delegateBindings.map((item) => item.profileId);
171
+ const orderedIds = [
172
+ input.installation.canonicalProfileId,
173
+ ...profileIds.filter((profileId) => profileId !== input.installation.canonicalProfileId).sort(),
174
+ ];
175
+ const profiles = orderedIds.map((profileId) => (
176
+ knownPaths.get(profileId) || join(input.clientRoot, "profiles", `${profileId}.json`)
177
+ ));
178
+ await writeNeutralHostProfileManifest({ root: input.neutralHostRoot, profiles });
179
+ }
180
+
181
+ async function doctor(options, dependencies = {}) {
182
+ const root = options.root || defaultHelloLynkClientRoot();
183
+ const profileId = required(options, "profile");
184
+ const configPath = options.config || join(root, "profiles", `${profileId}.json`);
185
+ const profile = await readClientProfile(configPath);
186
+ if (profile.profileId !== profileId) throw new Error("client_profile_binding_mismatch");
187
+ const store = dependencies.store || createPlatformCredentialStore({ storeRoot: join(root, "credentials") });
188
+ let credentialPresent = false;
189
+ try {
190
+ let credential = await store.load(profileId);
191
+ credentialPresent = Boolean(credential);
192
+ credential = null;
193
+ } catch {
194
+ credentialPresent = false;
195
+ }
196
+ return runSanitizedDoctor({
197
+ baseUrl: profile.baseUrl,
198
+ allowLoopbackHttp: profile.allowLoopbackHttp,
199
+ credentialPresent,
200
+ platform: profile.platform,
201
+ fetchImpl: dependencies.fetchImpl,
202
+ });
203
+ }
204
+
205
+ export async function runRelayClientCli(argv = process.argv.slice(2), dependencies = {}) {
206
+ const { command, options } = parseArgs(argv);
207
+ if (command === "connect") {
208
+ const allowed = new Set(["base-url", "root", "neutral-host-root", "allow-loopback-http", "hellolynk-id", "adapter", "profile", "instance-name", "device-label"]);
209
+ if (Object.keys(options).some((key) => !allowed.has(key))) throw new Error("device_enrollment_argument_unknown");
210
+ return createDeviceEnrollmentClient({
211
+ baseUrl: required(options, "base-url"), root: options.root, hostRoot: options["neutral-host-root"],
212
+ allowLoopbackHttp: options["allow-loopback-http"] === true,
213
+ }, dependencies).connect({
214
+ hellolynkId: required(options, "hellolynk-id"), clientType: required(options, "adapter"),
215
+ profileId: options.profile, requestedName: options["instance-name"], deviceLabel: options["device-label"],
216
+ });
217
+ }
218
+ if (command === "bind") return bind(options, dependencies);
219
+ if (command === "doctor") return doctor(options, dependencies);
220
+ throw new Error("client_command_invalid");
221
+ }
222
+
223
+ const directRun = process.argv[1] && basename(process.argv[1]) === basename(fileURLToPath(import.meta.url));
224
+ if (directRun) {
225
+ runRelayClientCli().then(
226
+ (result) => process.stdout.write(`${JSON.stringify(result, null, 2)}\n`),
227
+ (error) => {
228
+ process.stderr.write(`${JSON.stringify({ ok: false, error: publicErrorCode(error) })}\n`);
229
+ process.exitCode = 1;
230
+ },
231
+ );
232
+ }
@@ -0,0 +1,160 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execute = promisify(execFile);
8
+ const clients = ['codex', 'openclaw', 'trae-work', 'workbuddy', 'dsh', 'claude-code'];
9
+ const contract = 'hellolynk-release-retention-v1';
10
+ const failure = code => Object.assign(new Error(code), { code });
11
+ const equal = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
12
+
13
+ async function directory(target) {
14
+ const stat = await fs.lstat(target);
15
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw failure('release_retention_path_invalid');
16
+ return stat;
17
+ }
18
+
19
+ async function rootPath(root) {
20
+ if (!path.isAbsolute(root || '') || path.basename(root) !== 'releases') throw failure('release_retention_root_invalid');
21
+ const resolved = path.resolve(root);
22
+ for (let p = resolved; ; p = path.dirname(p)) {
23
+ await directory(p);
24
+ if (p === path.dirname(p)) break;
25
+ }
26
+ // Windows may supply an 8.3 TEMP/user path. Parents were checked above;
27
+ // delete only under its canonical absolute spelling.
28
+ return fs.realpath(resolved);
29
+ }
30
+
31
+ async function regularTree(target) {
32
+ await directory(target);
33
+ for (const item of await fs.readdir(target, { withFileTypes: true })) {
34
+ const child = path.join(target, item.name), stat = await fs.lstat(child);
35
+ if (stat.isSymbolicLink()) throw failure('release_retention_link');
36
+ if (stat.isDirectory()) await regularTree(child);
37
+ else if (!stat.isFile() || stat.nlink !== 1) throw failure('release_retention_file_invalid');
38
+ }
39
+ }
40
+
41
+ // Read only release references, never return process command lines. A running
42
+ // older Host may still import files after installation; deletion waits for the
43
+ // next successful install instead of stopping any user process.
44
+ export async function runningReleaseNames(releasesRoot) {
45
+ if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
46
+ const root = await rootPath(releasesRoot);
47
+ const literal = `${root}${path.sep}`.replaceAll("'", "''");
48
+ const script = `$ErrorActionPreference='Stop'; $pattern=[regex]::Escape('${literal}')+'((?:codex|openclaw|trae-work|workbuddy|dsh|claude-code)-[a-f0-9]{64})(?=[\\\\/"\\s]|$)'; $names=@(Get-CimInstance Win32_Process | ForEach-Object { if ($_.CommandLine) { foreach ($m in [regex]::Matches($_.CommandLine,$pattern,'IgnoreCase')) { $m.Groups[1].Value.ToLowerInvariant() } } }); ConvertTo-Json -Compress -InputObject @($names | Sort-Object -Unique)`;
49
+ const shell = path.join(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
50
+ const result = await execute(shell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
51
+ { windowsHide: true, timeout: 15000, maxBuffer: 256 * 1024 });
52
+ const names = JSON.parse(result.stdout.replace(/^\uFEFF/, '').trim());
53
+ if (!Array.isArray(names) || names.some(n => typeof n !== 'string' || !/^(?:codex|openclaw|trae-work|workbuddy|dsh|claude-code)-[a-f0-9]{64}$/.test(n))) {
54
+ throw failure('release_retention_process_check_invalid');
55
+ }
56
+ return names;
57
+ }
58
+
59
+ // A stopped on-demand task is still a configuration reference. Keeping only
60
+ // live processes can delete its executable before the next client startup.
61
+ export async function configuredReleaseNames(releasesRoot) {
62
+ if (process.platform !== 'win32') throw failure('release_retention_configuration_check_unavailable');
63
+ const root = await rootPath(releasesRoot);
64
+ const prefixes = [...new Set([root, path.resolve(releasesRoot)])].map(value => (value + path.sep).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
65
+ const pattern = ('(?:' + prefixes.join('|') + ')((?:codex|openclaw|trae-work|workbuddy|dsh|claude-code)-[a-f0-9]{64})(?=[\\\\/"\\s]|$)').replaceAll("'", "''");
66
+ const script = "$ErrorActionPreference='Stop'; $pattern='" + pattern + "'; $scheduler=New-Object -ComObject 'Schedule.Service'; $scheduler.Connect(); $names=@($scheduler.GetFolder('\\').GetTasks(1) | Where-Object { $_.Name -like 'HelloLynk *' } | ForEach-Object { foreach ($a in $_.Definition.Actions) { foreach ($v in @($a.Path,$a.Arguments,$a.WorkingDirectory)) { foreach ($m in [regex]::Matches([string]$v,$pattern,'IgnoreCase')) { $m.Groups[1].Value.ToLowerInvariant() } } } }); ConvertTo-Json -Compress -InputObject @($names | Sort-Object -Unique)";
67
+ const shell = path.join(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
68
+ const { stdout } = await execute(shell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
69
+ { windowsHide: true, timeout: 15000, maxBuffer: 256 * 1024 });
70
+ const names = JSON.parse(stdout.replace(/^\uFEFF/, '').trim());
71
+ if (!Array.isArray(names) || names.some(n => typeof n !== 'string' || !/^(?:codex|openclaw|trae-work|workbuddy|dsh|claude-code)-[a-f0-9]{64}$/.test(n))) {
72
+ throw failure('release_retention_configuration_check_invalid');
73
+ }
74
+ return names;
75
+ }
76
+
77
+ // Call only after the caller's complete adoption transaction succeeds, while
78
+ // holding its install/account lock. Never call from candidate staging or a
79
+ // rollback. The receipt records adoption order, not directory timestamps.
80
+ // Cleanup failure is reported separately and must not roll back a good install.
81
+ export async function pruneInstalledReleases({ releasesRoot, client, currentRelease, previousRelease = null, referencedReleases = [] }, dependencies = {}) {
82
+ const result = { complete: false, retained: [], removed: [], deferred: [] };
83
+ try {
84
+ if (!clients.includes(client)) throw failure('release_retention_client_invalid');
85
+ const root = await rootPath(releasesRoot), pattern = new RegExp(`^${client}-[a-f0-9]{64}$`);
86
+ const nameOf = value => {
87
+ if (typeof value !== 'string' || !path.isAbsolute(value)
88
+ || ![root, path.resolve(releasesRoot)].some(p => equal(path.dirname(path.resolve(value)), p))
89
+ || !pattern.test(path.basename(value))) throw failure('release_retention_target_invalid');
90
+ return path.basename(value);
91
+ };
92
+ const current = nameOf(currentRelease);
93
+ const receiptPath = path.join(root, `.${client}-retention.json`);
94
+ let old = null;
95
+ try {
96
+ const stat = await fs.lstat(receiptPath);
97
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 4096) throw failure('release_retention_receipt_invalid');
98
+ old = JSON.parse(await fs.readFile(receiptPath, 'utf8'));
99
+ if (old?.contract !== contract || old.client !== client || !pattern.test(old.current)
100
+ || old.previous !== null && !pattern.test(old.previous) || old.current === old.previous) throw failure('release_retention_receipt_invalid');
101
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
102
+ const supplied = previousRelease === null ? null : nameOf(previousRelease);
103
+ const previous = supplied && supplied !== current ? supplied
104
+ : old?.current === current ? old.previous : old?.current || null;
105
+ const retained = [current, previous].filter(Boolean);
106
+ for (const name of retained) await directory(path.join(root, name));
107
+ const referenced = new Set([...referencedReleases.map(nameOf),
108
+ ...await (dependencies.configuredReleases || configuredReleaseNames)(releasesRoot)]);
109
+ const running = new Set(await (dependencies.runningReleases || runningReleaseNames)(root));
110
+ // Publish before deleting. An interrupted cleanup can retry the exact same
111
+ // pair on same-version reinstall without accidentally dropping its backup.
112
+ const temporary = `${receiptPath}.${crypto.randomUUID()}.tmp`;
113
+ try {
114
+ await fs.writeFile(temporary, JSON.stringify({ contract, client, current, previous }) + '\n', { flag: 'wx', mode: 0o600 });
115
+ await fs.rename(temporary, receiptPath);
116
+ } finally { await fs.unlink(temporary).catch(e => { if (e.code !== 'ENOENT') throw e; }); }
117
+ result.retained = retained;
118
+ await removeReleases(root, name => pattern.test(name) && !retained.includes(name), referenced, running, result, dependencies);
119
+ result.complete = result.deferred.length === 0;
120
+ } catch (error) { result.error = safeCode(error); }
121
+ return result;
122
+ }
123
+
124
+ // Uninstall keeps no release. One a process or scheduled task still uses is
125
+ // left for the next install or uninstall, like any other deferred release.
126
+ export async function removeInstalledReleases({ releasesRoot, client }, dependencies = {}) {
127
+ const result = { complete: false, removed: [], deferred: [] };
128
+ try {
129
+ if (!clients.includes(client)) throw failure('release_retention_client_invalid');
130
+ try { await fs.lstat(releasesRoot); } catch (error) { if (error.code === 'ENOENT') return { ...result, complete: true }; throw error; }
131
+ const root = await rootPath(releasesRoot), pattern = new RegExp(`^${client}-[a-f0-9]{64}$`);
132
+ const referenced = new Set(await (dependencies.configuredReleases || configuredReleaseNames)(releasesRoot));
133
+ const running = new Set(await (dependencies.runningReleases || runningReleaseNames)(root));
134
+ await removeReleases(root, name => pattern.test(name), referenced, running, result, dependencies);
135
+ if (!result.deferred.length) await fs.rm(path.join(root, `.${client}-retention.json`), { force: true });
136
+ result.complete = result.deferred.length === 0;
137
+ } catch (error) { result.error = safeCode(error); }
138
+ return result;
139
+ }
140
+
141
+ async function removeReleases(root, selected, referenced, running, result, dependencies) {
142
+ for (const name of await fs.readdir(root)) {
143
+ if (!selected(name)) continue;
144
+ if (referenced.has(name)) { result.deferred.push({ name, reason: 'release_still_referenced' }); continue; }
145
+ if (running.has(name)) { result.deferred.push({ name, reason: 'release_in_use' }); continue; }
146
+ try {
147
+ // The final absolute target is a checked direct child of the canonical
148
+ // releases root; never follow a junction inside an obsolete package.
149
+ const target = path.resolve(root, name);
150
+ if (!equal(path.dirname(target), root) || !equal(await fs.realpath(target), target)) throw failure('release_retention_path_invalid');
151
+ await regularTree(target);
152
+ await (dependencies.remove || fs.rm)(target, { recursive: true });
153
+ result.removed.push(name);
154
+ } catch (error) { result.deferred.push({ name, reason: safeCode(error) }); }
155
+ }
156
+ }
157
+
158
+ function safeCode(error) {
159
+ return /^(?:release_retention_[a-z_]+|E[A-Z]+)$/.test(error?.code || '') ? error.code : 'release_retention_failed';
160
+ }