@hellolynk-ai/trae-work-mcp 0.0.0-stage → 0.4.94

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/LICENSE +92 -0
  2. package/README.md +86 -2
  3. package/bin/hellolynk-client-setup.mjs +2748 -0
  4. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  5. package/hellolynk-client-pack-manifest.json +619 -0
  6. package/hellolynk-client-pack-pin.json +4 -0
  7. package/integrations/trae-work/package.json +11 -0
  8. package/integrations/trae-work/runtime/neutral-host-bootstrap.mjs +38 -0
  9. package/integrations/trae-work/runtime/neutral-host-release-entry.mjs +16 -0
  10. package/integrations/trae-work/runtime/trae-work-bootstrap.mjs +30 -0
  11. package/integrations/trae-work/server/dynamic-ui-contract.mjs +376 -0
  12. package/integrations/trae-work/server/host-lifecycle.mjs +128 -0
  13. package/integrations/trae-work/server/mcp-proxy.mjs +181 -0
  14. package/integrations/trae-work/server/mcp-server.mjs +784 -0
  15. package/integrations/trae-work/server/native-task-cdp.mjs +241 -0
  16. package/integrations/trae-work/server/native-task-worker.mjs +199 -0
  17. package/integrations/trae-work/server/neutral-host-adapter.mjs +725 -0
  18. package/integrations/trae-work/server/trae-neutral-production.mjs +97 -0
  19. package/integrations/trae-work/server/trae-ui-launch.mjs +182 -0
  20. package/integrations/trae-work/server/ui-action-gate.mjs +122 -0
  21. package/integrations/trae-work/skills/hellolynk-inbox/SKILL.md +9 -0
  22. package/package.json +61 -4
  23. package/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  24. package/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  25. package/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  26. package/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  27. package/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  28. package/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  29. package/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  30. package/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  31. package/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  32. package/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  33. package/runtime/connect-activate.mjs +837 -0
  34. package/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  35. package/runtime/core/hellolynk-client-account.mjs +180 -0
  36. package/runtime/core/hellolynk-client-core.mjs +770 -0
  37. package/runtime/core/hellolynk-client-profile.mjs +191 -0
  38. package/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  39. package/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  40. package/runtime/core/hellolynk-client-update.mjs +453 -0
  41. package/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  42. package/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  43. package/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  44. package/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  45. package/runtime/core/hellolynk-host-auth.mjs +347 -0
  46. package/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  47. package/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  48. package/runtime/core/hellolynk-host.mjs +2374 -0
  49. package/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  50. package/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  51. package/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  52. package/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  53. package/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  54. package/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  55. package/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  56. package/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  57. package/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  58. package/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  59. package/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  60. package/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  61. package/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  62. package/runtime/core/hellolynk-model-inbox.mjs +230 -0
  63. package/runtime/core/hellolynk-model-notices.cjs +60 -0
  64. package/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  65. package/runtime/core/hellolynk-native-conversations.mjs +280 -0
  66. package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  67. package/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  68. package/runtime/core/hellolynk-native-notice.mjs +50 -0
  69. package/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  70. package/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  71. package/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  72. package/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  73. package/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  74. package/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  75. package/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  76. package/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  77. package/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  78. package/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  79. package/runtime/core/hellolynk-release-retention.mjs +160 -0
  80. package/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  81. package/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  82. package/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  83. package/runtime/neutral-host-bootstrap.mjs +38 -0
  84. package/runtime/neutral-host-release-entry.mjs +16 -0
  85. package/runtime/neutral-host-release-loader.mjs +156 -0
  86. package/runtime/neutral-host-release-manifest.json +394 -0
  87. package/runtime/neutral-host-release-pin.json +4 -0
  88. package/runtime/platform/windows/configure-agent.ps1 +281 -0
  89. package/runtime/platform/windows/credential-store.ps1 +844 -0
  90. package/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  91. package/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  92. package/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  93. package/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  94. package/runtime/trae-work-bootstrap.mjs +30 -0
  95. package/runtime/workbench/candidate-bootstrap.mjs +54 -0
  96. package/runtime/workbench/contract.mjs +1314 -0
  97. package/runtime/workbench/hellolynk-workbench-build.json +215 -0
  98. package/runtime/workbench/host-capabilities.mjs +80 -0
  99. package/runtime/workbench/i18n.mjs +1127 -0
  100. package/runtime/workbench/loopback-surface.mjs +740 -0
  101. package/runtime/workbench/mcp-app-bundle.mjs +97 -0
  102. package/runtime/workbench/service.mjs +485 -0
  103. package/runtime/workbench/state.mjs +421 -0
  104. package/runtime/workbench/transports.mjs +171 -0
  105. package/runtime/workbench/ui/app.mjs +6120 -0
  106. package/runtime/workbench/ui/connection-form.mjs +155 -0
  107. package/runtime/workbench/ui/hellolynk-mark.png +0 -0
  108. package/runtime/workbench/ui/http-transport.mjs +247 -0
  109. package/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  110. package/runtime/workbench/ui/icons/close.svg +3 -0
  111. package/runtime/workbench/ui/icons/contacts.svg +3 -0
  112. package/runtime/workbench/ui/icons/inbox.svg +3 -0
  113. package/runtime/workbench/ui/icons/refresh.svg +3 -0
  114. package/runtime/workbench/ui/icons/search.svg +3 -0
  115. package/runtime/workbench/ui/icons/security.svg +3 -0
  116. package/runtime/workbench/ui/icons/settings.svg +3 -0
  117. package/runtime/workbench/ui/index.html +139 -0
  118. package/runtime/workbench/ui/loopback-entry.mjs +26 -0
  119. package/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  120. package/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  121. package/runtime/workbench/ui/styles.css +686 -0
  122. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
  123. package/tools/hellolynk-install-client-skills.mjs +1211 -0
@@ -0,0 +1,97 @@
1
+ import path from "node:path";
2
+ import { resolveClientAccountRoot } from "../../../runtime/core/hellolynk-client-account.mjs";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+
5
+ import { createHostProtectedStore } from "../../../runtime/core/hellolynk-host-protected-store.mjs";
6
+ import { createNeutralHostInstallationStore, defaultNeutralHostStoreRoot } from "../../../runtime/core/hellolynk-neutral-host-installation-store.mjs";
7
+ import { launchTraeWorkNeutralHost, verifyPinnedBootstrap } from "./host-lifecycle.mjs";
8
+
9
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ const RELEASE_ROOT = path.resolve(MODULE_DIR, "..", "..", "..");
11
+
12
+ // This is intentionally TRAE-owned: it obtains the protected binding locally,
13
+ // checks that it points at this fixed release, and starts no other client host.
14
+ export async function createTraeWorkNeutralProduction(options = {}) {
15
+ const releaseRoot = path.resolve(options.releaseRoot || RELEASE_ROOT);
16
+ const { storeRoot, protectedStore } = traeStores(options);
17
+ const resolved = options.resolvedBinding || await protectedStore.resolveTraeWorkNeutralBinding();
18
+ assertReleaseBinding(resolved, releaseRoot);
19
+ const preverified = String(options.verifiedReleaseManifestSha256 || "");
20
+ if (preverified && preverified !== resolved.releaseManifestSha256) {
21
+ throw codeError("neutral_trae_work_runner_integrity_invalid");
22
+ }
23
+ const inspection = preverified
24
+ ? Object.freeze({ ok: true, preverified: true })
25
+ : await verifyPinnedBootstrap(resolved, { storeRoot });
26
+ let initialBinding = resolved;
27
+ let launchCandidate = null;
28
+ return Object.freeze({
29
+ inspection, releaseRoot, storeRoot, protectedStore,
30
+ capabilityProvider: async () => {
31
+ const binding = initialBinding || await protectedStore.resolveTraeWorkNeutralBinding();
32
+ initialBinding = null;
33
+ assertReleaseBinding(binding, releaseRoot);
34
+ launchCandidate = binding;
35
+ return binding;
36
+ },
37
+ hostLauncher: async (binding) => {
38
+ if (!launchCandidate) throw codeError("neutral_trae_work_capability_binding_mismatch");
39
+ assertSamePublicBinding(binding, launchCandidate);
40
+ launchCandidate = null;
41
+ return launchTraeWorkNeutralHost(binding, {
42
+ storeRoot,
43
+ ...(options.spawnRunner ? { spawnRunner: options.spawnRunner } : {}),
44
+ ...(options.wait ? { wait: options.wait } : {}),
45
+ });
46
+ },
47
+ });
48
+ }
49
+
50
+ // A background update can move the binding to a newer release while this MCP stays open.
51
+ // Each connection uses the production of the release the binding names now; that module
52
+ // checks the binding against its own location again.
53
+ export async function createCurrentTraeWorkNeutralProduction(options = {}) {
54
+ const { protectedStore } = traeStores(options);
55
+ const resolved = await protectedStore.resolveTraeWorkNeutralBinding();
56
+ const ownRoot = path.resolve(options.releaseRoot || RELEASE_ROOT);
57
+ const boundRoot = path.resolve(String(resolved?.releaseRoot || ownRoot));
58
+ if (boundRoot === ownRoot) return createTraeWorkNeutralProduction({ ...options, protectedStore, resolvedBinding: resolved });
59
+ // Installed releases sit side by side; a binding that names any other place is not loaded.
60
+ if (path.dirname(boundRoot) !== path.dirname(ownRoot)) {
61
+ resolved?.capability?.secretBuffer?.fill(0);
62
+ throw codeError("neutral_trae_work_runner_integrity_invalid");
63
+ }
64
+ const { releaseRoot: _ownRoot, ...forwarded } = options;
65
+ const bound = await import(pathToFileURL(path.join(boundRoot, "integrations", "trae-work", "server", "trae-neutral-production.mjs")).href);
66
+ return bound.createTraeWorkNeutralProduction({ ...forwarded, resolvedBinding: resolved });
67
+ }
68
+
69
+ function traeStores(options) {
70
+ const env = options.env || process.env;
71
+ const storeRoot = resolveClientAccountRoot("trae-work", absolutePath(
72
+ options.storeRoot || env.HELLOLYNK_NEUTRAL_STORE_ROOT || defaultNeutralHostStoreRoot("win32", env),
73
+ "neutral_trae_work_binding_configuration_required",
74
+ ));
75
+ const installationStore = options.installationStore || createNeutralHostInstallationStore({ root: storeRoot });
76
+ return { storeRoot, protectedStore: options.protectedStore || createHostProtectedStore({ root: storeRoot, installationStore }) };
77
+ }
78
+
79
+ function assertReleaseBinding(binding, releaseRoot) {
80
+ if (!binding?.releaseRoot || path.resolve(binding.releaseRoot) !== releaseRoot
81
+ || !/^[a-f0-9]{64}$/.test(String(binding.releaseManifestSha256 || ""))
82
+ || binding.runnerRelativePath !== "runtime/neutral-host-bootstrap.mjs"
83
+ || !path.isAbsolute(String(binding.profileManifestPath || ""))) {
84
+ throw codeError("neutral_trae_work_runner_integrity_invalid");
85
+ }
86
+ }
87
+ function assertSamePublicBinding(expected, actual) {
88
+ for (const key of [
89
+ "region", "principalId", "delegateId", "hostInstallationId", "profileId", "adapterId", "version",
90
+ "runtimeRoot", "releaseRoot", "releaseManifestSha256", "runnerRelativePath", "profileManifestPath",
91
+ ]) {
92
+ if (expected?.[key] !== actual?.[key]) throw codeError("neutral_trae_work_capability_binding_mismatch");
93
+ }
94
+ if (JSON.stringify(expected.scopes) !== JSON.stringify(actual.scopes)) throw codeError("neutral_trae_work_capability_binding_mismatch");
95
+ }
96
+ function absolutePath(value, code) { const text = String(value || ""); if (!path.isAbsolute(text)) throw codeError(code); return path.resolve(text); }
97
+ function codeError(code) { const error = new Error(code); error.code = code; return error; }
@@ -0,0 +1,182 @@
1
+ import { createWorkbenchAccountOpener, createWorkbenchConnection, workbenchConnectionError, isWorkbenchConnectionRequired } from "../../../runtime/core/hellolynk-workbench-connection.mjs";
2
+ import path from "node:path";
3
+ import { lstat } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ let active = null;
7
+ const PACKAGED_WORKBENCH = new URL("../../../runtime/workbench/candidate-bootstrap.mjs", import.meta.url);
8
+ const SOURCE_WORKBENCH = new URL("../../../runtime/workbench/candidate-bootstrap.mjs", import.meta.url);
9
+ const ACTION_GRANT = /^[A-Za-z0-9._~-]{32,512}$/;
10
+
11
+ // TRAE SOLO CN rejects user VSIX extensions in its current Solo-Lite runtime,
12
+ // but its official Browser plugin can open loopback pages inside the client.
13
+ // This launcher returns only a clean 127.0.0.1 URL. Each explicit open request
14
+ // arms one top-level browser navigation, which becomes a short HttpOnly local
15
+ // session without putting a reusable secret in MCP, the URL, or the clipboard.
16
+ export async function openTraeWorkInbox(options = {}) {
17
+ const adapter = options.adapter;
18
+ requireReadAdapter(adapter);
19
+ if (typeof adapter.start === "function") await adapter.start().catch((error) => { if (!isWorkbenchConnectionRequired(error)) throw error; });
20
+ if (active && active.adapter === adapter) {
21
+ active.candidate.surface.armNavigation();
22
+ return active.result;
23
+ }
24
+ if (active) await closeTraeWorkInbox();
25
+ const sharedWorkbench = options.bootstrapWorkbenchCandidate
26
+ ? injectedSharedWorkbench(options)
27
+ : await loadSharedWorkbench();
28
+ const workbenchAdapter = createTraeWorkbenchAdapterFacade(adapter, sharedWorkbench.contract);
29
+ const bootstrapWorkbenchCandidate = sharedWorkbench.bootstrapWorkbenchCandidate;
30
+ const candidate = await bootstrapWorkbenchCandidate("trae-work", {
31
+ openAccount: options.openAccount || createWorkbenchAccountOpener(),
32
+ connectAccount: options.connectAccount || createWorkbenchConnection({ client: "trae-work", releaseRoot: path.resolve(fileURLToPath(new URL("../../../", import.meta.url))) }),
33
+ adapter: workbenchAdapter,
34
+ ...(options.port == null ? {} : { port: options.port }),
35
+ pagePath: "/inbox",
36
+ });
37
+ const result = Object.freeze({
38
+ launched: true,
39
+ cleanUrl: candidate.cleanUrl,
40
+ status: "ready_for_browser_navigation",
41
+ });
42
+ active = { adapter, candidate, result };
43
+ return result;
44
+ }
45
+
46
+ export async function closeTraeWorkInbox() {
47
+ if (!active) return;
48
+ const current = active; active = null;
49
+ await current.candidate.close();
50
+ }
51
+
52
+ export function createTraeWorkbenchAdapterFacade(adapter, contract) {
53
+ requireReadAdapter(adapter);
54
+ const facade = {
55
+ readSnapshot: (input) => Promise.resolve().then(() => adapter.readWorkbenchSnapshot(input)).catch((error) => { throw workbenchConnectionError(error); }),
56
+ };
57
+ if (typeof adapter.readWorkbenchChanges === "function") {
58
+ facade.readChanges = (input) => adapter.readWorkbenchChanges(input);
59
+ }
60
+ if (supportsF3Contract(contract) && hasHostActionGrantPair(adapter)) {
61
+ const issueWorkbenchActionGrant = adapter.issueWorkbenchActionGrant.bind(adapter);
62
+ const executeWorkbenchCommand = adapter.executeWorkbenchCommand.bind(adapter);
63
+ facade.issueActionGrant = async (command) => requireIssuedActionGrant(
64
+ await issueWorkbenchActionGrant(contract.validateWorkbenchCommand(command)),
65
+ "trae_workbench_action_grant_invalid",
66
+ );
67
+ facade.executeCommand = async (command, context) => {
68
+ const validated = contract.validateWorkbenchCommand(command);
69
+ const normalizedContext = validated.command === "message.send-status"
70
+ ? requireEmptyContext(context, "trae_workbench_action_grant_invalid")
71
+ : requireActionGrantContext(context, "trae_workbench_action_grant_required");
72
+ return contract.validateWorkbenchCommandResult(
73
+ await executeWorkbenchCommand(validated, normalizedContext),
74
+ validated.command,
75
+ );
76
+ };
77
+ }
78
+ return Object.freeze(facade);
79
+ }
80
+
81
+ async function loadSharedWorkbench() {
82
+ const moduleUrl = await selectWorkbenchModule();
83
+ let loaded;
84
+ let contract;
85
+ try {
86
+ [loaded, contract] = await Promise.all([
87
+ import(moduleUrl.href),
88
+ import(new URL("./contract.mjs", moduleUrl).href),
89
+ ]);
90
+ } catch {
91
+ throw codeError("trae_shared_workbench_unavailable");
92
+ }
93
+ if (typeof loaded.bootstrapWorkbenchCandidate !== "function") {
94
+ throw codeError("trae_shared_workbench_unavailable");
95
+ }
96
+ return Object.freeze({ bootstrapWorkbenchCandidate: loaded.bootstrapWorkbenchCandidate, contract });
97
+ }
98
+
99
+ function injectedSharedWorkbench(options) {
100
+ if (typeof options.bootstrapWorkbenchCandidate !== "function") {
101
+ throw codeError("trae_shared_workbench_unavailable");
102
+ }
103
+ return Object.freeze({
104
+ bootstrapWorkbenchCandidate: options.bootstrapWorkbenchCandidate,
105
+ contract: options.workbenchContract || null,
106
+ });
107
+ }
108
+
109
+ async function selectWorkbenchModule() {
110
+ const packaged = await regularFile(PACKAGED_WORKBENCH);
111
+ if (packaged) return PACKAGED_WORKBENCH;
112
+ if (await regularFile(SOURCE_WORKBENCH)) return SOURCE_WORKBENCH;
113
+ throw codeError("trae_shared_workbench_unavailable");
114
+ }
115
+
116
+ async function regularFile(url) {
117
+ try {
118
+ const metadata = await lstat(fileURLToPath(url));
119
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
120
+ throw codeError("trae_shared_workbench_unavailable");
121
+ }
122
+ return true;
123
+ } catch (error) {
124
+ if (error?.code === "ENOENT") return false;
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ function requireReadAdapter(adapter) {
130
+ if (!adapter || typeof adapter.readWorkbenchSnapshot !== "function") {
131
+ throw codeError("trae_shared_workbench_adapter_required");
132
+ }
133
+ }
134
+
135
+ function supportsF3Contract(value) {
136
+ return Boolean(value)
137
+ && value.HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT === "hellolynk-workbench-snapshot-v12"
138
+ && value.HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION === 12
139
+ && value.HELLOLYNK_WORKBENCH_COMMAND_CONTRACT === "hellolynk-workbench-command-v6"
140
+ && value.HELLOLYNK_WORKBENCH_RESULT_CONTRACT === "hellolynk-workbench-command-result-v6"
141
+ && value.HELLOLYNK_WORKBENCH_SCHEMA_VERSION === 6
142
+ && typeof value.validateWorkbenchCommand === "function"
143
+ && typeof value.validateWorkbenchCommandResult === "function";
144
+ }
145
+
146
+ function hasHostActionGrantPair(adapter) {
147
+ return typeof adapter.issueWorkbenchActionGrant === "function"
148
+ && adapter.issueWorkbenchActionGrant.length >= 1
149
+ && typeof adapter.executeWorkbenchCommand === "function"
150
+ && adapter.executeWorkbenchCommand.length >= 2;
151
+ }
152
+
153
+ function requireActionGrantContext(value, errorCode) {
154
+ if (!value || typeof value !== "object" || Array.isArray(value)
155
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
156
+ || Object.keys(value).length !== 1 || !Object.hasOwn(value, "actionGrant")
157
+ || typeof value.actionGrant !== "string" || !ACTION_GRANT.test(value.actionGrant)) {
158
+ throw codeError(errorCode);
159
+ }
160
+ return value;
161
+ }
162
+
163
+ function requireEmptyContext(value, errorCode) {
164
+ if (!value || typeof value !== "object" || Array.isArray(value)
165
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
166
+ || Object.keys(value).length !== 0) {
167
+ throw codeError(errorCode);
168
+ }
169
+ return value;
170
+ }
171
+
172
+ function requireIssuedActionGrant(value, errorCode) {
173
+ if (!value || typeof value !== "object" || Array.isArray(value)
174
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
175
+ || Object.keys(value).length !== 1 || !Object.hasOwn(value, "actionGrant")
176
+ || typeof value.actionGrant !== "string" || !ACTION_GRANT.test(value.actionGrant)) {
177
+ throw codeError(errorCode);
178
+ }
179
+ return value;
180
+ }
181
+
182
+ function codeError(code) { const error = new Error(code); error.code = code; return error; }
@@ -0,0 +1,122 @@
1
+ import crypto from "node:crypto";
2
+ import { validateDynamicUiEnvelope } from "./dynamic-ui-contract.mjs";
3
+
4
+ export const TRAE_PRIVATE_UI_CONTRACT = "hellolynk-trae-private-ui-v1";
5
+ export const TRAE_UI_ACTION_GATE_CONTRACT = "hellolynk-trae-ui-action-gate-v1";
6
+ export const TRAE_UI_ACTION_TOKEN_TTL_MS = 5 * 60 * 1000;
7
+ export const TRAE_UI_ACTION_TOKEN_MAX_ENTRIES = 2_048;
8
+
9
+ const TOKEN = /^hellolynkui_[A-Za-z0-9_-]{43}$/;
10
+ const TOOL = /^hellolynk_[a-z0-9_]{3,100}$/;
11
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/;
12
+ const MODES = new Set(["exact", "subset", "new-contact", "send-form", "no-target"]);
13
+
14
+ export function createTraeUiActionGate(options = {}) {
15
+ const now = typeof options.now === "function" ? options.now : Date.now;
16
+ const randomBytes = typeof options.randomBytes === "function" ? options.randomBytes : crypto.randomBytes;
17
+ const ttlMs = integer(options.ttlMs, TRAE_UI_ACTION_TOKEN_TTL_MS, 1_000, 15 * 60 * 1000);
18
+ const maxEntries = integer(options.maxEntries, TRAE_UI_ACTION_TOKEN_MAX_ENTRIES, 1, 10_000);
19
+ const entries = new Map();
20
+
21
+ function issue(policy = {}) {
22
+ cleanup();
23
+ if (entries.size >= maxEntries) throw codeError("trae_ui_action_gate_full");
24
+ const toolName = safeTool(policy.toolName);
25
+ const mode = String(policy.mode || "");
26
+ if (!MODES.has(mode)) throw codeError("trae_ui_action_policy_invalid");
27
+ const allowedIds = canonicalIds(policy.allowedIds || []);
28
+ if ((mode === "exact" && allowedIds.length !== 1)
29
+ || (mode === "subset" && allowedIds.length < 1)
30
+ || ((mode === "new-contact" || mode === "send-form" || mode === "no-target") && allowedIds.length > 500)) {
31
+ throw codeError("trae_ui_action_policy_invalid");
32
+ }
33
+ let token;
34
+ do { token = `hellolynkui_${randomBytes(32).toString("base64url")}`; } while (entries.has(digest(token)));
35
+ const issuedAtMs = now();
36
+ const expiresAtMs = issuedAtMs + ttlMs;
37
+ entries.set(digest(token), Object.freeze({ toolName, mode, allowedIds, issuedAtMs, expiresAtMs }));
38
+ return Object.freeze({
39
+ contract: TRAE_UI_ACTION_GATE_CONTRACT,
40
+ token,
41
+ toolName,
42
+ mode,
43
+ expiresAtMs,
44
+ });
45
+ }
46
+
47
+ function consume(tokenValue, intent = {}) {
48
+ cleanup();
49
+ const token = String(tokenValue || "");
50
+ if (!TOKEN.test(token)) throw codeError("trae_ui_action_authorization_required");
51
+ const key = digest(token);
52
+ const entry = entries.get(key);
53
+ if (!entry) throw codeError("trae_ui_action_authorization_invalid");
54
+ entries.delete(key);
55
+ if (entry.expiresAtMs <= now()) throw codeError("trae_ui_action_authorization_expired");
56
+ if (entry.toolName !== safeTool(intent.toolName)) throw codeError("trae_ui_action_scope_mismatch");
57
+ const targetIds = canonicalIds(intent.targetIds || []);
58
+ const allowed = new Set(entry.allowedIds);
59
+ if (entry.mode === "exact" && (targetIds.length !== 1 || targetIds[0] !== entry.allowedIds[0])) {
60
+ throw codeError("trae_ui_action_scope_mismatch");
61
+ }
62
+ if (entry.mode === "subset" && (!targetIds.length || targetIds.some((id) => !allowed.has(id)))) {
63
+ throw codeError("trae_ui_action_scope_mismatch");
64
+ }
65
+ if (entry.mode === "new-contact" && targetIds.length !== 0) throw codeError("trae_ui_action_scope_mismatch");
66
+ if (entry.mode === "no-target" && targetIds.length !== 0) throw codeError("trae_ui_action_scope_mismatch");
67
+ if (entry.mode === "send-form") {
68
+ const recipientKind = String(intent.recipientKind || "");
69
+ if (recipientKind === "contact") {
70
+ if (targetIds.length !== 1 || !allowed.has(targetIds[0])) throw codeError("trae_ui_action_scope_mismatch");
71
+ } else if (recipientKind === "typed") {
72
+ if (targetIds.length !== 0) throw codeError("trae_ui_action_scope_mismatch");
73
+ } else throw codeError("trae_ui_action_scope_mismatch");
74
+ }
75
+ return Object.freeze({ ok: true, consumed: true, toolName: entry.toolName, mode: entry.mode });
76
+ }
77
+
78
+ function cleanup() {
79
+ const current = now();
80
+ for (const [key, entry] of entries) if (entry.expiresAtMs <= current) entries.delete(key);
81
+ }
82
+
83
+ return Object.freeze({ issue, consume, get size() { cleanup(); return entries.size; } });
84
+ }
85
+
86
+ export function createPrivateUiMeta(widgetEnvelope, actionGrants = []) {
87
+ try { validateDynamicUiEnvelope(widgetEnvelope); }
88
+ catch { throw codeError("trae_private_ui_envelope_invalid"); }
89
+ if (!Array.isArray(actionGrants) || actionGrants.length > 16) throw codeError("trae_private_ui_grants_invalid");
90
+ return Object.freeze({
91
+ hellolynkTraePrivateUi: Object.freeze({
92
+ contract: TRAE_PRIVATE_UI_CONTRACT,
93
+ modelVisibility: "none",
94
+ widgetEnvelope,
95
+ actionGrants: Object.freeze(actionGrants.map(validateGrant)),
96
+ }),
97
+ });
98
+ }
99
+
100
+ function validateGrant(value) {
101
+ if (!value || typeof value !== "object" || Array.isArray(value)
102
+ || value.contract !== TRAE_UI_ACTION_GATE_CONTRACT || !TOKEN.test(String(value.token || ""))
103
+ || !TOOL.test(String(value.toolName || "")) || !MODES.has(String(value.mode || ""))
104
+ || !Number.isSafeInteger(value.expiresAtMs) || value.expiresAtMs < 0) {
105
+ throw codeError("trae_private_ui_grants_invalid");
106
+ }
107
+ return Object.freeze({
108
+ contract: value.contract, token: value.token, toolName: value.toolName,
109
+ mode: value.mode, expiresAtMs: value.expiresAtMs,
110
+ });
111
+ }
112
+
113
+ function canonicalIds(value) {
114
+ if (!Array.isArray(value) || value.length > 500) throw codeError("trae_ui_action_policy_invalid");
115
+ const ids = value.map((item) => { const id = String(item || ""); if (!ID.test(id)) throw codeError("trae_ui_action_policy_invalid"); return id; }).sort();
116
+ if (new Set(ids).size !== ids.length) throw codeError("trae_ui_action_policy_invalid");
117
+ return Object.freeze(ids);
118
+ }
119
+ function safeTool(value) { const text = String(value || ""); if (!TOOL.test(text)) throw codeError("trae_ui_action_policy_invalid"); return text; }
120
+ function digest(token) { return crypto.createHash("sha256").update(token).digest("hex"); }
121
+ function integer(value, fallback, minimum, maximum) { const number = Number(value ?? fallback); if (!Number.isSafeInteger(number) || number < minimum || number > maximum) throw codeError("trae_ui_action_gate_limit_invalid"); return number; }
122
+ function codeError(code) { const error = new Error(code); error.code = code; return error; }
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: hellolynk-inbox
3
+ description: 在 TRAE Work 中打开和使用 HelloLynk 通讯;不用于邮件。
4
+ ---
5
+ # HelloLynk 通讯
6
+ HelloLynk 是本人授权的 AI 通讯。接着已有来往用 hellolynk_reply_authorized_message,沿用来信的 localMessageId 和 conversationKey;发起新通信用 hellolynk_send_authorized_message,contact 填对方名字。每条新消息用新的 clientMessageId;发送失败或结果未知时保留编号并停止,不重发;参数无效时改正后重新调用。收到本聊天的 HelloLynk 自动提醒时,用 hellolynk_claim_message 按 batchId 读一次。没有本聊天的提醒或本人查信要求时,不主动查信。用户要看全部来信时,打开本客户端的 HelloLynk 界面供本人查看。
7
+ ## 打开
8
+ 1. 调用 hellolynk_open_inbox(空参数)一次。
9
+ 2. 用 OpenPreview 打开返回的地址。
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@hellolynk-ai/trae-work-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.94",
4
+ "description": "HelloLynk Messaging for TRAE Work: open conversations and let your AI message other people's AI assistants.",
5
+ "homepage": "https://hellolynk.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/haoyun18881-beep/HelloLynk.git",
9
+ "directory": "clients/trae-work"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/haoyun18881-beep/HelloLynk/issues"
13
+ },
14
+ "keywords": [
15
+ "hellolynk-ai",
16
+ "HelloLynk",
17
+ "agent-communication",
18
+ "trae-work"
19
+ ],
20
+ "license": "SEE LICENSE IN LICENSE",
21
+ "type": "module",
22
+ "os": [
23
+ "win32"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22.16.0"
27
+ },
28
+ "files": [
29
+ "LICENSE",
30
+ "README.md",
31
+ "bin/",
32
+ "tools/",
33
+ "integrations/trae-work/",
34
+ "runtime/",
35
+ "hellolynk-client-pack-manifest.json",
36
+ "hellolynk-client-pack-pin.json"
37
+ ],
38
+ "bin": {
39
+ "hellolynk-trae-work": "./bin/hellolynk-client-setup.mjs"
40
+ },
41
+ "scripts": {
42
+ "connect": "node ./bin/hellolynk-client-setup.mjs connect",
43
+ "init": "node ./bin/hellolynk-client-setup.mjs init",
44
+ "activate": "node ./bin/hellolynk-client-setup.mjs activate --binding-key-from-stdin",
45
+ "doctor": "node ./bin/hellolynk-client-setup.mjs doctor",
46
+ "setup": "node ./bin/hellolynk-client-setup.mjs setup"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "registry": "https://registry.npmjs.org/"
51
+ },
52
+ "hellolynkClient": {
53
+ "client": "trae-work",
54
+ "activationTool": "tools/hellolynk-activate-neutral-host-clients.mjs",
55
+ "coreRoot": "runtime/core",
56
+ "profileId": "hellolynk-host-trae-work-v2",
57
+ "releaseRoot": ".",
58
+ "serviceBaseUrl": "https://hellolynk.com",
59
+ "configurator": "runtime/core/hellolynk-agent-configurator.mjs",
60
+ "entry": "runtime/trae-work-bootstrap.mjs",
61
+ "skillTarget": "trae"
62
+ }
63
+ }
@@ -0,0 +1,180 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { lstatSync, readFileSync } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { applyCurrentUserAcl, defaultNeutralHostStoreRoot, withNeutralHostStoreLock } from "./hellolynk-neutral-host-installation-store.mjs";
6
+
7
+ export const ACCOUNT_CLIENTS = Object.freeze(["codex", "openclaw", "trae-work", "workbuddy", "dsh", "claude-code"]);
8
+ const GENERATION = /^[a-f0-9]{32}$/;
9
+ const INITIAL = "initial";
10
+
11
+ // Packages and account data have separate lifetimes. Existing installations
12
+ // keep their shared account until the user disconnects one particular client.
13
+ export function clientAccountBaseRoot(root = defaultNeutralHostStoreRoot()) {
14
+ if (!path.isAbsolute(root)) throw failure("workbench_account_path_invalid");
15
+ root = path.resolve(root);
16
+ const generation = path.dirname(root), client = path.dirname(generation), accounts = path.dirname(client);
17
+ if (path.basename(root) === "host" && GENERATION.test(path.basename(generation))
18
+ && ACCOUNT_CLIENTS.includes(path.basename(client)) && path.basename(accounts) === "account-data") {
19
+ return path.join(path.dirname(accounts), "host");
20
+ }
21
+ return root;
22
+ }
23
+
24
+ function statePath(client, baseRoot) {
25
+ if (!ACCOUNT_CLIENTS.includes(client)) throw failure("workbench_account_client_invalid");
26
+ return path.join(baseRoot, `client-account-${client}.json`);
27
+ }
28
+
29
+ export function readClientAccount(client, root = defaultNeutralHostStoreRoot()) {
30
+ const baseRoot = clientAccountBaseRoot(root);
31
+ const file = statePath(client, baseRoot);
32
+ let record;
33
+ try {
34
+ assertPath(file);
35
+ const stat = lstatSync(file);
36
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024) throw failure("workbench_account_state_invalid");
37
+ record = JSON.parse(readFileSync(file, "utf8"));
38
+ } catch (error) {
39
+ if (error.code !== "ENOENT") throw failure("workbench_account_state_invalid");
40
+ return Object.freeze({ client, baseRoot, root: baseRoot, revision: INITIAL, phase: "ready", previousRevision: null });
41
+ }
42
+ if (record?.version !== 1 || !GENERATION.test(record.revision)
43
+ || !["disconnecting", "ready"].includes(record.phase)
44
+ || (record.previousRevision !== INITIAL && !GENERATION.test(record.previousRevision))
45
+ || Object.keys(record).sort().join(",") !== "phase,previousRevision,revision,version") {
46
+ throw failure("workbench_account_state_invalid");
47
+ }
48
+ return Object.freeze({ client, baseRoot, root: generationRoot(client, baseRoot, record.revision), ...record });
49
+ }
50
+
51
+ export function resolveClientAccountRoot(client, root) {
52
+ const account = readClientAccount(client, root);
53
+ if (account.phase !== "ready") throw failure("workbench_account_disconnect_incomplete");
54
+ return account.root;
55
+ }
56
+
57
+ export function assertClientAccountCurrent(client, root) {
58
+ if (!ACCOUNT_CLIENTS.includes(client)) return;
59
+ const account = readClientAccount(client, root);
60
+ if (account.phase !== "ready" || !samePath(root, account.root)) throw failure("workbench_account_changed");
61
+ }
62
+
63
+ export function withClientAccountLock(client, root, operation) {
64
+ const baseRoot = clientAccountBaseRoot(root);
65
+ statePath(client, baseRoot);
66
+ return withNeutralHostStoreLock(baseRoot, `client-account-${client}`, operation);
67
+ }
68
+
69
+ export function createClientAccountActions(options) {
70
+ const client = options?.client;
71
+ const root = options?.storeRoot || defaultNeutralHostStoreRoot("win32", options?.env || process.env);
72
+ statePath(client, clientAccountBaseRoot(root));
73
+ const readAccountState = () => {
74
+ const state = readClientAccount(client, root);
75
+ return { revision: state.revision, disconnecting: state.phase === "disconnecting", canDisconnect: true };
76
+ };
77
+ const assertRevision = (revision) => {
78
+ const state = readClientAccount(client, root);
79
+ if (state.revision !== revision) throw failure("workbench_account_changed");
80
+ if (state.phase !== "ready") throw failure("workbench_account_disconnect_incomplete");
81
+ return state.root;
82
+ };
83
+ return Object.freeze({
84
+ readAccountState, assertRevision,
85
+ runForAccount(revision, operation) {
86
+ return withClientAccountLock(client, root, () => operation(assertRevision(revision)));
87
+ },
88
+ async runSafetyVerificationForAccount(revision, operation) {
89
+ // Only native safety verification may wait outside the account lock.
90
+ // The Host fences its eventual write with the original attachment and
91
+ // exact device identity; the account revision also fences its UI result.
92
+ const pending = await withClientAccountLock(client, root, () => ({
93
+ completion: Promise.resolve(operation(assertRevision(revision))).then(
94
+ (value) => ({ value }), (error) => ({ error }),
95
+ ),
96
+ }));
97
+ const outcome = await pending.completion;
98
+ return withClientAccountLock(client, root, () => {
99
+ assertRevision(revision);
100
+ if (Object.hasOwn(outcome, "error")) throw outcome.error;
101
+ return outcome.value;
102
+ });
103
+ },
104
+ async disconnectAccount(input) {
105
+ if (!input || Object.keys(input).join(",") !== "revision"
106
+ || typeof input.revision !== "string" || !/^(?:initial|[a-f0-9]{32})$/.test(input.revision)) {
107
+ throw failure("workbench_disconnect_input_invalid");
108
+ }
109
+ return disconnectClientAccount({ client, root, revision: input.revision }, async (previousRoot) => {
110
+ const { createHostProtectedStore } = await import("./hellolynk-host-protected-store.mjs");
111
+ await createHostProtectedStore({ root: previousRoot, followClientAccount: false,
112
+ ...(options.helperPath ? { helperPath: options.helperPath } : {}),
113
+ }).disconnectLocalClient(client);
114
+ });
115
+ },
116
+ });
117
+ }
118
+
119
+ // The pending record is a durable fence: a crash while revoking the old local
120
+ // capability must never make it usable again. Retrying resumes that same fence.
121
+ export async function disconnectClientAccount({ client, root, revision }, revokePrevious) {
122
+ return withClientAccountLock(client, root, async () => {
123
+ let account = readClientAccount(client, root);
124
+ if (account.phase === "ready" && account.previousRevision === revision) {
125
+ return { disconnected: true, revision: account.revision };
126
+ }
127
+ if (revision !== account.revision && !(account.phase === "disconnecting" && revision === account.previousRevision)) {
128
+ throw failure("workbench_account_changed");
129
+ }
130
+ if (account.phase === "ready") {
131
+ await save(client, account.baseRoot, {
132
+ version: 1, revision: randomBytes(16).toString("hex"), previousRevision: account.revision, phase: "disconnecting",
133
+ });
134
+ account = readClientAccount(client, root);
135
+ }
136
+ const previousRoot = generationRoot(client, account.baseRoot, account.previousRevision);
137
+ await revokePrevious(previousRoot);
138
+ await save(client, account.baseRoot, {
139
+ version: 1, revision: account.revision, previousRevision: account.previousRevision, phase: "ready",
140
+ });
141
+ return { disconnected: true, revision: account.revision };
142
+ });
143
+ }
144
+
145
+ function generationRoot(client, baseRoot, revision) {
146
+ return revision === INITIAL ? baseRoot : path.join(path.dirname(baseRoot), "account-data", client, revision, "host");
147
+ }
148
+
149
+ async function save(client, root, record) {
150
+ const file = statePath(client, root);
151
+ assertPath(root);
152
+ const temporary = `${file}.${process.pid}.${randomBytes(6).toString("hex")}`;
153
+ let handle;
154
+ try {
155
+ handle = await fs.open(temporary, "wx", 0o600);
156
+ await handle.writeFile(`${JSON.stringify(record)}\n`);
157
+ await handle.sync();
158
+ await handle.close(); handle = null;
159
+ await fs.chmod(temporary, 0o600);
160
+ await applyCurrentUserAcl(temporary, false);
161
+ await fs.rename(temporary, file);
162
+ } finally {
163
+ await handle?.close();
164
+ await fs.unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; });
165
+ }
166
+ }
167
+
168
+ function assertPath(file) {
169
+ let cursor = path.resolve(file);
170
+ while (cursor !== path.dirname(cursor)) {
171
+ try { if (lstatSync(cursor).isSymbolicLink()) throw failure("workbench_account_path_invalid"); }
172
+ catch (error) { if (error.code !== "ENOENT") throw error; }
173
+ cursor = path.dirname(cursor);
174
+ }
175
+ }
176
+ function samePath(a, b) {
177
+ const normalize = (v) => process.platform === "win32" ? path.resolve(v).toLowerCase() : path.resolve(v);
178
+ return normalize(a) === normalize(b);
179
+ }
180
+ function failure(code) { return Object.assign(new Error(code), { code, status: 409 }); }